【问题标题】:Casting issue in call of custom aggregate function in dynamic SQL在动态 SQL 中调用自定义聚合函数时的强制转换问题
【发布时间】:2021-01-26 15:14:50
【问题描述】:

我正在尝试创建一个检查函数来检查 tsrange 是否包含在另一个中。考虑complete example in this fiddle

最后一节是做“魔术”,这里是功能的 MVP。 在上面的例子中可以找到一个实际的执行。

-- Setting up init stuff
CREATE TABLE country(entity_id BIGSERIAL PRIMARY KEY, valid tsrange, registration tsrange);
CREATE TABLE airport(entity_id BIGSERIAL PRIMARY KEY, valid tsrange, registration tsrange,
country_key bigint references country (entity_id));


-- persisting data into tables
INSERT INTO country 
VALUES(1,'[2016-06-22 19:10:25-07, 2019-01-01 00:00:00)', tsrange(now()::timestamp, '9999-01-01 00:00:00'));
INSERT INTO airport VALUES(1,'[2018-06-22 19:10:25-07, 2020-01-01 00:00:00)', tsrange(now()::timestamp, '9999-01-01 00:00:00'), 1);

-- range_merge wrapper
CREATE OR REPLACE AGGREGATE range_merge(anyrange)
(
    sfunc = range_merge,
    stype = anyrange
);


-- Aggregate function for aggregating all validity periods from select statement. 
CREATE OR REPLACE FUNCTION aggregate_validity(entity_name regclass, entry bigint) returns tsrange AS
$$
DECLARE
    result tsrange;
BEGIN
   EXECUTE format('select range_merge(valid) from %I where entity_id = %s', entity_name, entry) into result;
   return result;
END
$$ LANGUAGE plpgsql;


- Detect integrity breach function 
CREATE OR REPLACE FUNCTION ingoing_outgoing_reference_integrity_breach(altered_table_name text, entity_ids bigint[])  
RETURNS TABLE(
       source_entity text,
       source_entry_id bigint,
       source_entry_validity tsrange,
       is_included_in_timeline boolean,
       aggregated_foreing_entry_validity tsrange,
       foreing_entry_id bigint,
       foreing_entity text,
       is_reference_ingoing boolean
      )
AS $$
DECLARE
    foreign_key  record;
    is_reference_ingoing boolean;
BEGIN

    FOR foreign_key  IN (
        --  Using the information schema from postgres, all constraints of type 
        --  FOREIGN_KEY, is extracted. For each of the foreing key constraint, the 
        --  source_table and foreing_table which is linked by this constraint is then extracted.  
        --  to ensure both ingoing references and outgoing references are extracted from this query, is
        --  condition provided as such that either the source_table = altered_table_name OR foreing_table_name = altered_table_name||'_registration'
        SELECT
            tc.table_name AS source_table, 
            kcu.column_name as column_attribute,
            ccu.table_name AS foreign_table
        FROM 
            information_schema.table_constraints AS tc 
            JOIN information_schema.key_column_usage AS kcu
            ON tc.constraint_name = kcu.constraint_name
            JOIN information_schema.constraint_column_usage AS ccu
            ON ccu.constraint_name = tc.constraint_name
        WHERE 
            constraint_type = 'FOREIGN KEY' and 
            (ccu.table_name = altered_table_name OR tc.table_name = altered_table_name) and
            ccu.column_name != 'row_id' and 
            kcu.column_name != 'entity_id') 
    LOOP
        DROP TABLE IF EXISTS lookup_breach;
        EXECUTE format('CREATE TEMP TABLE lookup_breach ON COMMIT DROP AS
                       SELECT '||foreign_key.source_table||'.entity_id AS source_entry_id, -- The Id of the source entry
                    '||foreign_key.source_table||'.'||foreign_key.column_attribute||' AS foreing_entry_id, -- The id of the foreing entry
                    '||foreign_key.source_table||'.valid AS source_entry_valid, -- the validity period of the source entry
                    aggregate_validity('||foreign_key.foreign_table||', '||foreign_key.source_table||'.'||foreign_key.column_attribute||') AS foreing_entry_valid, -- the validity period of the foreing entry
                    aggregate_validity('||foreign_key.foreign_table||', '||foreign_key.source_table||'.'||foreign_key.column_attribute||') <@ '||foreign_key.source_table||'.valid as in_lifespan
                    FROM '||foreign_key.foreign_table||', '||foreign_key.source_table||'
                    WHERE '||foreign_key.source_table||'.'||foreign_key.column_attribute||' = ANY($1)
                    AND '||foreign_key.source_table||'.'||foreign_key.column_attribute||' = '||foreign_key.foreign_table||'_registration.entity_id
                    AND '||foreign_key.source_table||'.registration @> now()::timestamp
                    GROUP BY '||foreign_key.source_table||'.'||foreign_key.column_attribute||', 
                    '||foreign_key.source_table||'.entity_id,'||foreign_key.source_table||'.valid,
                    '||foreign_key.foreign_table||'.valid' ) using entity_ids;

        RETURN QUERY SELECT foreign_key.source_table::text, lookup_breach.source_entry_id, lookup_breach.source_entry_valid, lookup_breach.in_lifespan , lookup_breach.foreing_entry_valid,
        lookup_breach.foreing_entry_id, foreign_key.foreign_table::text, foreign_key.foreign_table::text= altered_table_name AS is_reference_ingoing
        FROM lookup_breach;
    END LOOP;
END
$$ LANGUAGE plpgsql;

调用函数时出现此错误:

select * from ingoing_outgoing_reference_integrity_breach('country'::text, '{1}')
ERROR:  function aggregate_validity(country, bigint) does not exist
LINE 5:      aggregate_validity(country, airport.country_key) AS for...
             ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
QUERY:  CREATE TEMP TABLE lookup_breach ON COMMIT DROP AS
                       SELECT airport.entity_id AS source_entry_id, -- The Id of the source entry
                    airport.country_key AS foreing_entry_id, -- The id of the foreing entry
                    airport.valid AS source_entry_valid, -- the validity period of the source entry
                    aggregate_validity(country, airport.country_key) AS foreing_entry_valid, -- the validity period of the foreing entry
                    aggregate_validity(country, airport.country_key) <@ airport.valid as in_lifespan
                    FROM country, airport
                    WHERE airport.country_key = ANY($1)
                    AND airport.country_key = country_registration.entity_id
                    AND airport.registration @> now()::timestamp
                    GROUP BY airport.country_key, 
                    airport.entity_id,airport.valid,
                    country.valid
CONTEXT:  PL/pgSQL function ingoing_outgoing_reference_integrity_breach(text,bigint[]) line 33 at EXECUTE

好像我需要某种类型的选角?但我不知道该怎么做。

【问题讨论】:

  • 包含交互式“小提琴”很棒,但是您能否将edit 示例也包含在问题本身中,以防将来链接中断?还要确保您已将其归结为minimal reproducible example,方法是删除与您所询问的问题无关的原始任务的所有细节。
  • 我添加了示例。并且示例本身应该是 MRE @IMSoP
  • 我认为c1olumn_attribute 只是一个错字,应该是column_attribute?

标签: sql postgresql sql-injection dynamic-sql


【解决方案1】:

我不知道你想用上面的代码做什么。但是您收到错误是因为您使用错误的参数调用函数ingoing_outgoing_reference_integrity_breach(根据您的小提琴)。您将表名称提供为'coutry',但您应该将其提供为'"country"'。 所以你的select 声明应该如下所示:

select * from ingoing_outgoing_reference_integrity_breach('"country"'::text, '{1}')

【讨论】:

  • 不,这不是多个问题之一。
【解决方案2】:

您将自定义函数定义为aggregate_validity(regclass, bigint)

country 类型的值与 regclass 类型不同。这就是错误消息所抱怨的:

ERROR:  function aggregate_validity(country, bigint) does not exist
LINE 5:      aggregate_validity(country, airport.country_key) AS for...

在传递给聚合函数时,您可以将字符串文字 'country' 转换为 regclass

更重要的是,你已经使用format(),所以不要手动连接SQL字符串,引入SQL注入的向量。标识符在字面上连接和执行是不安全的!见:

您也不需要为循环的每次迭代创建一个临时表。

并且没有单独的RETURN QUERY。将其合并到 RETURN QUERY EXECUTE 的单个实例中。

类似:

CREATE OR REPLACE FUNCTION ingoing_outgoing_reference_integrity_breach(_altered_table_name text, _entity_ids bigint[])  
  RETURNS TABLE(source_entity                      text
              , source_entry_id                    bigint
              , source_entry_validity              tsrange
              , is_included_in_timeline            boolean
              , aggregated_foreign_entry_validity  tsrange
              , foreign_entry_id                   bigint
              , foreign_entity                     text
              , is_reference_ingoing               boolean)
  LANGUAGE plpgsql AS
$func$
DECLARE
   foreign_key record;
   is_reference_ingoing boolean;

BEGIN
   FOR foreign_key IN
      SELECT tc.table_name   AS source_table
           , kcu.column_name
           , ccu.table_name  AS foreign_table
      FROM   information_schema.table_constraints       tc 
      JOIN   information_schema.key_column_usage        kcu USING (constraint_name)
      JOIN   information_schema.constraint_column_usage ccu USING (constraint_name)
      WHERE  tc.constraint_type = 'FOREIGN KEY'
      AND    _altered_table_name IN (ccu.table_name, tc.table_name)
      AND    ccu.column_name <> 'row_id'
      AND    kcu.column_name <> 'entity_id'

   LOOP
      RETURN QUERY EXECUTE format(
         'SELECT %1$L                                              -- AS source_entity
               , %1$I.entity_id                                    -- AS source_entry_id     -- the ID of the source entry
               , %1$I.valid                                        -- AS source_entry_valid, -- the validity period of the source entry
               , aggregate_validity(%3$L::regclass, %1$I.%2$I) <@ %1$I.valid -- AS in_lifespan
               , aggregate_validity(%3$L::regclass, %1$I.%2$I)               -- AS foreign_entry_valid -- the validity period of the foreign entry
               , %1$I.%2$I                                         -- AS foreign_entry_id    -- the ID of the foreign entry
               , $1                                                -- AS foreign_entity
               , $2                                                -- AS is_reference_ingoing
          FROM   %3$I, %1$I
          WHERE  %1$I.%2$I = ANY($3)
          AND    %1$I.%2$I = %3$I.entity_id  -- %3$I_registration.entity_id ???
          AND    %1$I.registration @> now()::timestamp
          GROUP  BY %1$I.entity_id
                  , %1$I.valid
                  , %1$I.%2$I
                  , %3$I.valid'  -- WHY this one ???
      , foreign_key.source_table    -- %1$I and %1$L
      , foreign_key.column_name     -- %2$I
      , foreign_key.foreign_table   -- %3$I and %3$L
      )
      USING foreign_key.foreign_table::text                        -- $1
          , foreign_key.foreign_table::text = _altered_table_name  -- $2
          , _entity_ids;                                           -- $3;
   END LOOP;
END
$func$;

db小提琴here

我修复了所有拼写错误的“foreign”实例。

注意regclass 的显式转换。它不是严格需要的,因为正确引用的字符串文字(%3$L 中的格式说明符 L)现在将允许函数解析启动并自动转换为 regclass。 (您可以尝试不使用演员表。)

关于format()中的格式说明符:

可能会进一步简化,我没有深入挖掘。

【讨论】:

    猜你喜欢
    • 2016-11-01
    • 1970-01-01
    • 2013-03-29
    • 2020-03-09
    • 2011-05-21
    • 1970-01-01
    • 2016-02-26
    • 2020-05-15
    • 2018-10-15
    相关资源
    最近更新 更多