1

I'm working with Postgres and PostGIS. Trying to write a function that that selects specific columns according to the given argument.

I'm using a WITH statement to create the result table before converting it to bytea to return.
The part I need help with is the $4 part. I tried it is demonstrated below and $4::text and both give me back the text value of the input and not the column value in the table if cols=name so I get back from the query name and not the actual names in the table. I also try data($4) and got type error.
The code is like this:

CREATE OR REPLACE FUNCTION select_by_txt(z integer,x integer,y integer, cols text)
        RETURNS bytea
        LANGUAGE 'plpgsql'
    
AS $BODY$
declare
res bytea;
begin
    WITH bounds AS (
      SELECT ST_TileEnvelope(z, x, y) AS geom
    ),
    mvtgeom AS (
      SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, $4
      FROM table1 t, bounds
      WHERE ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
    )
    
    SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
    INTO res
    FROM mvtgeom;
    RETURN res;
end;
$BODY$;

Example for calling the function:

select_by_txt(10,32,33,"col1,col2")

The argument cols can be multiple column names from 1 and not limited from above. The names of the columns inside cols will be checked before calling the function that they are valid columns.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
Yoad
  • 35
  • 6
  • 1
    `cols` sounds like you are trying to pass *multiple* column names. Is that so? How many can it be? Please show example calls. – Erwin Brandstetter May 02 '21 at 15:41
  • yes, I've updated the post with an example as you ask @ErwinBrandstetter the answer below of JGH works for me with just one column – Yoad May 03 '21 at 10:21

2 Answers2

4

Passing multiple column names as concatenated string for dynamic execution urgently requires decontamination. I suggest a VARIADIC function parameter instead, with properly quoted identifiers (using quote_ident() in this case):

CREATE OR REPLACE FUNCTION select_by_txt(z int, x int, y int, VARIADIC cols text[] = NULL, OUT res text)
  LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format(
$$
SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
FROM  (
   SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom%s
   FROM   table1 t
   JOIN  (SELECT ST_TileEnvelope($1, $2, $3)) AS bounds(geom)
          ON ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
   ) mvtgeom
$$, (SELECT ', ' || string_agg(quote_ident (col), ', ') FROM unnest(cols) col)
   )
   INTO  res
   USING z, x, y;
END
$func$;

db<>fiddle here

The format specifier %I for format() deals with a single identifier. You have to put in more work for multiple identifiers, especially for a variable number of 0-n identifiers. This implementation quotes every single column name, and only add a , if any column names have been passed. So it works for every possible input, even no input at all. Note VARIADIC cols text[] = NULL as last input parameter with NULL as default value:

Related:

Column names are case sensitive in this context!

Call for your example (important!):

SELECT select_by_txt(10,32,33,'col1', 'col2');

Alternative syntax:

SELECT select_by_txt(10,32,33, VARIADIC '{col1,col2}');

More revealing call, with a third column name and malicious (though futile) intent:

SELECT select_by_txt(10,32,33,'col1', 'col2', $$col3'); DROP TABLE table1;--$$);

About that odd third column name and SQL injection:

About VAIRADIC parameters:

Using an OUT parameter for simplicity. That's totally optional. See:

What I would not do

If you really, really trust the input to be a properly formatted list of 1 or more valid column names at all times - and you asserted that ...

the names of the columns inside cols will be checked before calling the function that they are valid columns

You could simplify:

CREATE OR REPLACE FUNCTION select_by_txt(z int, x int, y int, cols text, OUT res text)
  LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format(
$$
SELECT ST_AsMVT(mvtgeom, 'public.select_by_txt')
FROM  (
   SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, %s
   FROM   table1 t
   JOIN  (SELECT ST_TileEnvelope($1, $2, $3)) AS bounds(geom)
          ON ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
   ) mvtgeom
$$, cols
   )
   INTO  res
   USING z, x, y;
END
$func$;

(How can you be so sure that the input will always be reliable?)

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
0

You would need to use a dynamic query:

CREATE OR REPLACE FUNCTION select_by_txt(z integer,x integer,y integer, cols text)
        RETURNS bytea
        LANGUAGE 'plpgsql'
    
AS $BODY$
declare
res bytea;
begin
EXECUTE format('
    WITH bounds AS (
      SELECT ST_TileEnvelope($1, $2, $3) AS geom
    ),
    mvtgeom AS (
      SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, %I
      FROM table1 t, bounds
      WHERE ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
    )
    
    SELECT ST_AsMVT(mvtgeom, ''public.select_by_txt'')
    FROM mvtgeom', cols)
    INTO res
    USING z,x,y;
    
    RETURN res;
end;
$BODY$;
JGH
  • 15,928
  • 4
  • 31
  • 48
  • Hi, First, thank you for responding! The answer you gave me is partly working for me. It's working when cols is single-column input but when cols='col1,co2' the function fails with this error - ERROR: column \"{col1,col2}\" does not exist (SQLSTATE 42703). there is a way to "tell" the function that cols is not a single column? – Yoad May 03 '21 at 10:52
  • @Yoad This answers your question as it was originally written. If you have a new question feel free to ask a new question in a new post. – JGH May 03 '21 at 11:30
  • The original question was for columns so it's still the same question... I've added an example because I saw that maybe this is not so clear. if this is important for you I can re-ask the question in a new post – Yoad May 03 '21 at 12:23