使用 plpgsql 存储过程返回 table

Returning a table with plpgsql stored procedure

我正在创建一个存储过程来复制在我们的地图服务器上的地图文件中找到的查询的输出。

查询:

select g.gid, g.geom, g.basin, a.\"DATE\", a.\"VALUE\" 
from sarffg_basins_00_regional_3sec as g 
   join \"%prod_table%\" as a on g.basin = a.\"BASIN\" 
where a.\"DATE\" = '%prod_date%'

由于 SO post 长度限制,我无法 post 整个输出,但重要的是此查询为我提供了带有列名的输出。调整语法以将常规 sql 匹配为 sql shell:

内的 运行
select g.gid, 
       g.geom, 
       g.basin, 
       a."DATE", 
       a."VALUE" from sarffg_basins_00_regional_3sec 
       as g 
       join "FFGS_PROCESSED_PRODUCT_ASM_SACSMA_UPPER_BASIN_TIMESERIES" 
       as a on g.basin = a."BASIN" where a."DATE" = '2017-01-12 18:00:00+00';

给出列

gid        |   geom      |     basin      |          DATE          | VALUE

我的存储过程有以下 return header:

RETURNS table (
    gid integer,
    geom geometry(MultiPolygon,4326),
    basin double precision,
    date timestamptz,
    value double precision
    )

和 return 语句:

    RETURN QUERY
    EXECUTE 'SELECT g.'||quote_ident('gid')||',
                    g.'||quote_ident('geom')||',
                    g.'||quote_ident('basin')||',
                    a.'||quote_ident('DATE')||',
                    a.'||quote_ident('VALUE')||'
                    FROM sarffg_basins_00_regional_3sec AS g JOIN '||quote_ident(prod_table)||' AS a
                    ON g.'||quote_ident('basin')||' = a.'||quote_ident('BASIN')||'
                    WHERE a.'||quote_ident('DATE')||' = '''||adj_timestamp||'''';

这给了我相同的输出,除了没有列名。我认为这是我的 mapfile 错误日志中出现此错误的原因:

Query error. Error executing query: ERROR:  column "VALUE" does not exist
LINE 1: select "VALUE",encode(ST_AsBinary(ST_Force2D("geom"),'NDR'),...

我知道列 "VALUE" 存在于 prod_table 中,特别是考虑到此函数在 psql shell 中调用时工作正常。

我需要做什么才能使我的输出包含 headers 列?

编辑:映射文件的更多上下文。

映射文件中的整个查询如下:

geom from (select g.gid, g.geom, g.basin, a.\"DATE\", a.\"VALUE\" from sarffg_basins_00_regional_3sec as g join \"%prod_table%\" as a on g.basin = a.\"BASIN\" where a.\"DATE\" = '%prod_date%') as subquery using unique gid using srid=4326

我基本上需要使用存储过程复制此功能,无论是在其整个过程中还是仅在子查询中。我的各种尝试可以在这里找到:。看来我最接近尝试仅复制子查询 - 即

geom from (select * from ingest_ffgs_prod_composite_csv('%prod_table%', 1484438400)) as subquery using unique gid using srid=4326

编辑 2:既然我花了更多时间考虑它,我不确定为什么 "VALUE" 会在查询仅从子查询中选择 "geom" 时丢失列 return 从我的存储过程中编辑。也许这毕竟不是错误的原因。

编辑 3:根据要求,这是整个函数。

CREATE OR REPLACE FUNCTION ingest_ffgs_prod_composite_csv(prod_table text, epoch_seconds bigint, original boolean DEFAULT false, roll_back boolean DEFAULT true)
RETURNS table (
    gid integer,
    geom geometry(MultiPolygon,4326),
    basin double precision,
    date timestamptz,
    VALUE double precision
    )
AS $$
DECLARE
    c01n text := 'BASIN';           c01t text := 'double precision';
    c02n text := 'MAP01';           c02t text := 'double precision';
    c03n text := 'MAP03';           c03t text := 'double precision';
    c04n text := 'MAP06';           c04t text := 'double precision';
    c05n text := 'MAP24';           c05t text := 'double precision';
    c06n text := 'GMAP06';          c06t text := 'double precision';
    c07n text := 'GMAP24';          c07t text := 'double precision';
    c08n text := 'ASMU06';          c08t text := 'double precision';
    c09n text := 'ASML06';          c09t text := 'double precision';
    c10n text := 'ASMT06';          c10t text := 'double precision';
    c11n text := 'FFG01';           c11t text := 'double precision';
    c12n text := 'FFG03';           c12t text := 'double precision';
    c13n text := 'FFG06';           c13t text := 'double precision';
    c14n text := 'PREVFFG01';       c14t text := 'double precision';
    c15n text := 'PREVFFG03';       c15t text := 'double precision';
    c16n text := 'PREVFFG06';       c16t text := 'double precision';
    c17n text := 'FMAP01';          c17t text := 'double precision';
    c18n text := 'FMAP03';          c18t text := 'double precision';
    c19n text := 'FMAP06';          c19t text := 'double precision';
    c20n text := 'IFFT01';          c20t text := 'double precision';
    c21n text := 'IFFT03';          c21t text := 'double precision';
    c22n text := 'IFFT06';          c22t text := 'double precision';
    c23n text := 'PFFT01';          c23t text := 'double precision';
    c24n text := 'PFFT03';          c24t text := 'double precision';
    c25n text := 'PFFT06';          c25t text := 'double precision';
    c26n text := 'FFFT01';          c26t text := 'double precision';
    c27n text := 'FFFT03';          c27t text := 'double precision';
    c28n text := 'FFFT06';          c28t text := 'double precision';
    c29n text := 'PET06';           c29t text := 'double precision';
    tablename_csv text := 'temp_table_csv';
    tablename_ts  text := 'temp_table_ts';
    tablename_ret text := 'temp_table_ret';
    full_timestamp timestamptz;
    adj_timestamp_text text;
    adj_timestamp timestamptz;
    year  double precision;
    month double precision;
    day   double precision;
    hour  double precision;
    year_text  text;
    month_text text;
    day_text   text;
    hour_text  text;
    csv_base_path text;
    csv_filename  text;
    csv_file_full_path text;
    ret_rec record;
    product text;
    interval text;
    curr_basin int;
    curr_val double precision;
    rec record;
    existing_table boolean;
    existing_data  boolean := false;
    ret_geom geometry(MultiPolygon,4326);
BEGIN
    -- parse prod_table name to determine which product and time interval we need to build a timeseries for
    -------------------------------------------------------------------------------------------------------
    product  := split_part(prod_table, '_', 4);
    interval := rtrim(split_part(prod_table, '_', 6), 'HR');
    IF interval = 'UPPE' THEN -- asm interval reads as 'UPPE'. Default to 6 hours
            interval := '06';
    END IF;

    raise notice 'product: %', product;
    raise notice 'interval: %', interval;

    -- convert epoch seconds to timestamp for parsing
    -------------------------------------------------
    full_timestamp := to_timestamp(epoch_seconds);
    year  := date_part('year',  full_timestamp);
    month := date_part('month', full_timestamp);
    day   := date_part('day',   full_timestamp);

    IF roll_back THEN
            hour  := date_part('hour', full_timestamp) - (date_part('hour', full_timestamp)::integer % interval::int);
    ELSE
            hour := date_part('hour',   full_timestamp);
    END IF;

    year_text  := year;
    month_text := month;
    day_text   := day;
    hour_text  := hour;

    IF month < 10 THEN
            month_text := '0' || month;
    END IF;
    IF day < 10 THEN
            day_text := '0' || day;
    END IF;
    IF hour < 10 THEN
            hour_text := '0' || hour;
    END IF;

    adj_timestamp_text := year_text || '-' || month_text || '-' || day_text || ' ' || hour_text || ':00:00+00';
    adj_timestamp := adj_timestamp_text::timestamptz;

    raise notice 'year: %', year_text;
    raise notice 'month: %', month_text;
    raise notice 'day: %', day_text;
    raise notice 'hour: %', hour_text;

    -- check if table with the desired data already exists within the database
    --------------------------------------------------------------------------
    EXECUTE format('SELECT EXISTS (
                            SELECT 1
                            FROM pg_tables
                            WHERE schemaname = ''public''
                            AND   tablename  = '''||prod_table||''')') INTO existing_table;

    IF existing_table THEN
            EXECUTE format('SELECT EXISTS (
                                    SELECT 1
                                    FROM '||quote_ident(prod_table)||'
                                    WHERE '||quote_ident('DATE')||' = '''||adj_timestamp||''')') INTO existing_data;
    END IF;

    raise notice 'existing_table: %', existing_table;
    raise notice 'existing_data: %', existing_data;





    IF NOT existing_data THEN -- need to manually create tables from csv file

            -- construct full path to csv file
            ----------------------------------
            csv_base_path := '/SARFFG/EXP/DATA/EXPORTS/REGIONAL/'||year_text||'/'||month_text||'/'||day_text||'/COMPOSITE_CSV';

            IF original THEN
                    csv_filename  := year_text||month_text||day_text||'-'||hour_text||'00_ffgs_prod_composite_table_01hr_regional_original.csv';
            ELSE
                    csv_filename  := year_text||month_text||day_text||'-'||hour_text||'00_ffgs_prod_composite_table_01hr_regional.csv';
            END IF;

            csv_file_full_path := csv_base_path||'/'||csv_filename;
                              -----------------------------------------------
            raise notice 'csv file: %', csv_file_full_path;

            -- create temporary table to store CSV contents
            -----------------------------------------------
            EXECUTE format('CREATE TEMP TABLE '||tablename_csv||' ('||c01n||' '||c01t||',
                                                               '||c02n||' '||c02t||',
                                                               '||c03n||' '||c03t||',
                                                               '||c04n||' '||c04t||',
                                                               '||c05n||' '||c05t||',
                                                               '||c06n||' '||c06t||',
                                                               '||c07n||' '||c07t||',
                                                               '||c08n||' '||c08t||',
                                                               '||c09n||' '||c09t||',
                                                               '||c10n||' '||c10t||',
                                                               '||c11n||' '||c11t||',
                                                               '||c12n||' '||c12t||',
                                                               '||c13n||' '||c13t||',
                                                               '||c14n||' '||c14t||',
                                                               '||c15n||' '||c15t||',
                                                               '||c16n||' '||c16t||',
                                                               '||c17n||' '||c17t||',
                                                               '||c18n||' '||c18t||',
                                                               '||c19n||' '||c19t||',
                                                               '||c20n||' '||c20t||',
                                                               '||c21n||' '||c21t||',
                                                               '||c22n||' '||c22t||',
                                                               '||c23n||' '||c23t||',
                                                               '||c24n||' '||c24t||',
                                                               '||c25n||' '||c25t||',
                                                               '||c26n||' '||c26t||',
                                                               '||c27n||' '||c27t||',
                                                               '||c28n||' '||c28t||',
                                                               '||c29n||' '||c29t||') ON COMMIT DROP');

            EXECUTE format('COPY '||tablename_csv||' FROM '''||csv_file_full_path||''' DELIMITER '','' CSV HEADER');



            -- create temp timeseries table
            -------------------------------
            EXECUTE format('CREATE TEMP TABLE '||tablename_ts||' (date timestamptz, basin int, value double precision) ON COMMIT DROP');

            FOR rec in SELECT * FROM temp_table_csv
            LOOP
                    curr_basin := rec.basin;
                    IF product = 'MAP' THEN
                            IF interval = '01' THEN
                                    curr_val := rec.map01;
                            ELSIF interval = '03' THEN
                                    curr_val := rec.map03;
                            ELSIF interval = '06' THEN
                                    curr_val := rec.map06;
                            ELSIF interval = '24' THEN
                                    curr_val := rec.map24;
                            ELSE
                                    raise warning 'interval not found';
                                    EXIT;
                            END IF;
                    ELSIF product = 'ASM' THEN
                            IF interval = '06' THEN
                                    curr_val := rec.asmu06;
                            ELSE
                                    raise warning 'interval not found';
                                    EXIT;
                            END IF;
                    ELSIF product = 'FFG' THEN
                            IF interval = '01' THEN
                                    curr_val := rec.ffg01;
                            ELSIF interval = '03' THEN
                                    curr_val := rec.ffg03;
                            ELSIF interval = '06' THEN
                                    curr_val := rec.ffg06;
                            ELSE
                                    raise warning 'interval not found';
                                    EXIT;
                            END IF;
                    ELSIF product = 'FFT' THEN
                            IF interval = '01' THEN
                                    curr_val := rec.fft01;
                            ELSIF interval = '03' THEN
                                    curr_val := rec.fft03;
                            ELSIF interval = '06' THEN
                                    curr_val := rec.fft06;
                            ELSE
                                    raise warning 'interval not found';
                                    EXIT;
                            END IF;
                    ELSE
                            raise warning 'product not found';
                            EXIT;
                    END IF;

                    INSERT INTO temp_table_ts (date, basin, value) VALUES (adj_timestamp, curr_basin, curr_val);
            END LOOP;

            RETURN QUERY
            EXECUTE 'SELECT g.gid,
                            g.geom,
                            g.basin,
                            a.date,
                            a.value
                            FROM sarffg_basins_00_regional_3sec
                            AS g
                            JOIN '||tablename_ts||'
                            AS a
                            ON g.'||quote_ident('basin')||' = a.'||quote_ident('basin')||'
                            WHERE a.'||quote_ident('date')||' = '''||adj_timestamp||'''';
    ELSE

            RETURN QUERY
            EXECUTE format('SELECT g.gid,
                                   g.geom,
                                   g.basin,
                                   a."DATE",
                                   a."VALUE"
                                   FROM sarffg_basins_00_regional_3sec AS g
                                   JOIN %I AS a ON g.basin = a."BASIN"
                                   WHERE a."DATE" = ', prod_table)
                                   using adj_timestamp;

    END IF;

END;
$$ LANGUAGE plpgsql;

编辑 4:经过更多尽职调查后,我回想问题出在我的输出中缺少列名。我的色标明确引用 "VALUE",但显然没有列名它不知道哪个是 "VALUE"。

将我的查询更改为

select gid, geom, basin, "DATE", "VALUE" from ingest_ffgs_prod_composite_csv('%prod_table%', 1484438400)

给了我列名,但不幸的是没有解决我的问题。

你的问题太混乱了。不过,我确实注意到 带引号的标识符 存在问题。该函数在 return 类型中声明:

...
RETURNS table (
    ...
    <b>date</b> timestamptz,
    <b>VALUE</b> double precision
    )
...

但是函数调用不匹配:

select gid, geom, basin, "DATE", "VALUE" from ingest_ffgs_prod_composite_csv(...

这完美地解释了您的错误消息:

ERROR:  column "VALUE" does not exist

因为没有列"VALUE"。只有value(double-quoted或没有)。

所有未加引号的标识符在 Postgres 中都转换为小写。所以 VALUEvalue(但 不是 "VALUE""Value")是相同的 as标识符。但不是字符串。当通过 '%I' 传递给 quote_ident()format() 时,大小写很重要,因为它被转义了。所以:

a.'||quote_ident('DATE')||',
a.'||quote_ident('VALUE')||'

生成 a."DATE", a."VALUE",这与 a.DATE, a.VALUE.

不同

此外,PL/pgSQL 或 SQL 函数的 主体 中的列名在外部不可见。只有声明的 return 类型对函数调用很重要。

我通常建议不要将 datevalue 作为列名。 Both are allowed in Postgres, but reserved words in standard SQL.

使用合法的、lower-case、不带引号的标识符专门避免这种混淆。

  • Are PostgreSQL column names case-sensitive?