I am trying to define a box variable in a PostgreSQL function. Here is an arbitrary function that I've tried and it does not work:
CREATE OR REPLACE FUNCTION "public"."find_centroid"("argbase_x" int8, "argbase_y" int8, "arg_length" int)
RETURNS TABLE("cent_x" float8, "cent_y" float8) AS $BODY$
DECLARE
edge_x int8;
edge_y int8;
bounds text;
BEGIN
edge_x := "argbase_x" + 5;
edge_y := "argbase_y" + 5;
RETURN QUERY
SELECT sum(location[0])/count("location") as cent_x, sum(location[1])/count("location") as cent_y
from points
WHERE "location" <# box'(("argbase_x","argbase_y"),("argbase_x"+"arg_length","argbase_y"+"arg_length"))';
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
The issue is with how to build a box from arguments box'(("argbase_x","argbase_y"),("argbase_x"+"arg_length","argbase_y"+"arg_length"))'
I've tried various methods so far, like string concatenation for example. But nothing is working and I can't seem to find any resources online that offers an example on how to declare or define a geometric variable in a function.
After several trials and errors. This worked for me:
CREATE OR REPLACE FUNCTION "public"."find_centroid"("argbase_x" int8, "argbase_y" int8, "arg_length" int)
RETURNS TABLE("cent_x" float8, "cent_y" float8) AS $BODY$
DECLARE
edge_x int8;
edge_y int8;
bounds text;
BEGIN
edge_x := "argbase_x" + 5;
edge_y := "argbase_y" + 5;
bounds := CONCAT('((',"argbase_x"::text,',',"argbase_y"::text,'),
RETURN QUERY
SELECT sum(location[0])/count("location") as cent_x, sum(location[1])/count("location") as cent_y
from points
WHERE "location" <# box(bounds);
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
you don't have to define any special "box variable", bounding box is done simply as x BETWEEN min_x AND max_x AND y BETWEEN min_y AND max_y
if I understood the logic of the function correctly it would be
WHERE location[0] BETWEEN "argbase_x" AND "argbase_x"+"arg_length"
AND location[1] BETWEEN "argbase_y" AND "argbase_y"+"arg_length"
for more complex shapes there are no built-in operators in Postgres. You'll have to use WKT notation to construct your shapes and PostGIS functions to convert WKT to geometry type here and identify containment or intersection.
Related
SQL Server has a feature whereby you can call a function or stored procedure with a variable name for the func/proc name. Toy example:
declare #name sysname;
declare #method int = 1;
set #name = IIF(#method = 1, N'Newton', N'Taylor')
declare #sqrt float;
exec #sqrt = #name 42
This will call either Newton or Taylor depending on the value of #method. Using this, it is possible to implement the strategy or command OOP patterns in T-SQL. Where I work, we use it for just that purpose.
Now that I'm learning Postgresql, I'm wondering how I would do something similar in pgplsql. Any tips appreciated!
If all called functions return a single value of the same data type and take a single parameter of the same data type, you can do that with dynamic SQL in Postgres as well:
create or replace function evaluate(p_input integer, p_method text)
returns float
as
$$
declare
l_result float;
begin
execute 'select '||p_method||'($1)'
using p_input
into l_result;
return l_result;
end;
$$
language plpgsql;
select evaluate(42, 'sqrt'); returns 6.48074069840786
select evaluate(1, 'exp'); returns 2.718281828459045
This works with multiple parameters as well:
create or replace function evaluate(p_arg_1 integer, p_arg_2 text, p_method text)
returns float
as
$$
declare
l_result float;
begin
execute 'select '||p_method||'($1, $2)'
using p_arg_1, p_arg_2
into l_result;
return l_result;
end;
$$
language plpgsql;
I have a function to left pad bit stings in PostgreSQL 9.5:
CREATE OR REPLACE FUNCTION lpad_bits(val bit varying)
RETURNS bit varying as
$BODY$
BEGIN return val::bit(32) >> (32-length(val));
END;
$BODY$
LANGUAGE plpgsql IMMUTABLE;
which works fine:
# select lpad_bits(b'1001100111000');
lpad_bits
----------------------------------
00000000000000000001001100111000
(1 row)
My problem is when I try to add a parameter to change the amount of padding:
CREATE OR REPLACE FUNCTION lpad_bits(val bit varying, sz integer default 1024)
RETURNS bit varying as
$BODY$
BEGIN return val::bit(sz) >> (sz-length(val));
END;
$BODY$
LANGUAGE plpgsql IMMUTABLE;
The function is now broken:
# select lpad_bits(b'1001100111000', 32);
ERROR: invalid input syntax for integer: "sz"
LINE 1: SELECT val::bit(sz) >> (sz-length(val))
^
QUERY: SELECT val::bit(sz) >> (sz-length(val))
CONTEXT: PL/pgSQL function lpad_bits(bit varying,integer) line 2 at RETURN
I have stared at the bitstring documentation and PL/pgSQL function documentation, am simply not seeing what is fundamentally different between these two implementations.
Why?
PL/pgSQL executes SQL queries like prepared statements. The manual about parameter substituion:
Prepared statements can take parameters: values that are substituted
into the statement when it is executed.
Note the term values. Only actual values can be parameterized, but not key words, identifiers or type names. 32 in bit(32) looks like a value, but the modifier of a data type is only a "value" internally and can't be parameterized. SQL demands to know data types at planning stage, it cannot wait for the execution stage.
You could achieve your goal with dynamic SQL and EXECUTE. As proof of concept:
CREATE OR REPLACE FUNCTION lpad_bits(val varbit, sz int = 32, OUT outval varbit) AS
$func$
BEGIN
EXECUTE format('SELECT $1::bit(%s) >> $2', sz) -- literal
USING val, sz - length(val) -- values
INTO outval;
END
$func$ LANGUAGE plpgsql IMMUTABLE;
Call:
SELECT lpad_bits(b'1001100111000', 32);
Note the distinction between sz being used as literal to build the statement and its second occurrence where it's used as value, that can be passed as parameter.
Faster alternatives
A superior solution for this particular task is to just use lpad() like #Abelisto suggested:
CREATE OR REPLACE FUNCTION lpad_bits2(val varbit, sz int = 32)
RETURNS varbit AS
$func$
SELECT lpad(val::text, sz, '0')::varbit;
$func$ LANGUAGE sql IMMUTABLE;
(Simpler as plain SQL function, which also allows function inlining in the context of outer queries.)
Several times faster than the above function. A minor flaw: we have to cast to text and back to varbit. Unfortunately, lpad() is not currently implemented for varbit. The manual:
The following SQL-standard functions work on bit strings as well as
character strings: length, bit_length, octet_length, position, substring, overlay.
overlay() is available, we can have a cheaper function:
CREATE OR REPLACE FUNCTION lpad_bits3(val varbit, base varbit = '00000000000000000000000000000000')
RETURNS varbit AS
$func$
SELECT overlay(base PLACING val FROM bit_length(base) - bit_length(val))
$func$ LANGUAGE sql IMMUTABLE;
Faster if you can work with varbit values to begin with. (The advantage is (partly) voided, if you have to cast text to varbit anyway.)
Call:
SELECT lpad_bits3(b'1001100111000', '00000000000000000000000000000000');
SELECT lpad_bits3(b'1001100111000', repeat('0', 32)::varbit);
We might overlaod the function with a variant taking an integer to generate base itself:
CREATE OR REPLACE FUNCTION lpad_bits3(val varbit, sz int = 32)
RETURNS varbit AS
$func$
SELECT overlay(repeat('0', sz)::varbit PLACING val FROM sz - bit_length(val))
$func$ LANGUAGE sql IMMUTABLE;
Call:
SELECT lpad_bits3(b'1001100111000', 32;
Related:
Postgresql Convert bit varying to integer
Convert hex in text representation to decimal number
The parser does not allow a variable at that place. The alternative is to use a constant and trim it:
select right((val::bit(128) >> (128 -length(val)))::text, sz)::bit(sz)
from (values (b'1001100111000', 32)) s(val,sz)
;
right
----------------------------------
00000000000000000001001100111000
Or the lpad function as suggested in the comments.
I've been asked how to do a standard deviation on a variable list of values within a row. For example:
select
name, x, y, z, stddev (x, y, z)
from foo;
or
select
order_no, a, b, c, d, e, f, stddev (a, b, c, d, e, f)
from foo;
So essentially just like min => least and max => greatest, I'd like a similar way to turn the aggregate stddev into a "normal" function.
I have been able to create a custom function to calculate standard deviation based on the standard formula, but I can't help but prefer to use the built-in function, if possible. I tried this:
CREATE OR REPLACE FUNCTION std_deviation(variadic inputs numeric[])
RETURNS numeric AS
$BODY$
DECLARE
result numeric;
BEGIN
select stddev (unnest (inputs))
into result;
return result;
end
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
And it complains:
ERROR: set-valued function called in context that cannot accept a set
There is no shortage of traffic on this error message, but I can't quite figure out how to apply the fix to my simple function.
Or, is there a better way to do this from the beginning?
Set-returning functions (SRF) -- such as unnest -- in the SELECT clause is a PostgreSQL specific extension of the SQL standard. And usually doesn't worth to use it (because it's not what it looks like). Also, SRFs cannot be used within aggregate functions.
Use these SRF functions in the FROM clause instead (and use sub-selects where needed):
SELECT name, x, y, z, (SELECT stddev(v) FROM unnest(ARRAY[x, y, z]) v)
FROM foo
If you really want to write a function for that, use the SQL language (it's more clear & PostgreSQL can optimize their use):
CREATE OR REPLACE FUNCTION std_deviation(variadic inputs numeric[])
RETURNS numeric AS
$BODY$
SELECT stddev(v) FROM unnest(inputs) v
$BODY$
LANGUAGE SQL IMMUTABLE;
This seems to do the trick.
CREATE OR REPLACE FUNCTION public.std_deviation(VARIADIC inputs numeric[])
RETURNS numeric AS
$BODY$
DECLARE
result numeric;
BEGIN
with foo as (
select unnest (inputs) as bar
)
select stddev (bar)
into result
from foo;
return result;
end
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
It turns out pgnumerics already has a function for this.
-- test=# select pgnumerics.stdev('{1345,1301,1368,1322,1310,1370,1318,1350,1303,1299}');
-- stdev
-- ------------------
-- 27.4639157198435
-- (1 row)
CREATE OR REPLACE FUNCTION pgnumerics.stdev (
X double precision []
) RETURNS double precision
AS $$
DECLARE
s double precision;
N integer;
i integer;
xx double precision;
sx double precision;
BEGIN
N := array_upper(X,1) - array_lower(X,1) + 1;
xx:= 0.0;
sx:= 0.0;
for i in 1..N loop
xx:= xx + X[i]*X[i];
sx:= sx + X[i];
end loop;
s := sqrt((N*xx - sx*sx) / (N*(N-1.0)));
return s;
END;
$$ LANGUAGE 'plpgsql';
http://pgnumerics.projects.pgfoundry.org/
I have to convert from lat and long to geom to use PostGIS. My problem, I have various tables from different locations and I want to pass the table as a parameter to the function. I'm trying this:
CREATE or REPLACE FUNCTION convert_from_lon_lat(float,float,character varying)
RETURNS integer AS $$
select id from $3 as vertices
order by vertices.geom <-> ST_SetSrid(ST_MakePoint($1,$2),4326) LIMIT 1;
$$ LANGUAGE SQL;
but I get a syntax error.
EDIT1:
So I changed the previous code to this:
CREATE or REPLACE FUNCTION convert_from_lon_lat(long float, lat float, _table character varying) RETURNS integer AS $$
BEGIN
EXECUTE('select id from _table as vertices order by vertices.geom <-> ST_SetSrid(ST_MakePoint(long,lat),4326) LIMIT 1;');
END;
$$ LANGUAGE plpgsql;
it creates without any problem, but when I call it `convert_from_lon_lat(long1, long2, my_table)
I get and error:
ERROR: relation "_table" does not exist
It's not passing the table name as an argument
EDIT 2:
CREATE or REPLACE FUNCTION convert_from_lon_lat(long float, lat float, tbl character varying) RETURNS integer AS $func$
BEGIN
EXECUTE format('select id from %s order by %s.the_geom <-> ST_SetSrid(ST_MakePoint('|| long || ','|| lat ||'),4326) LIMIT 1;', tbl, tbl);
END;
$func$ LANGUAGE plpgsql;
Now when I call the function, I get an `ERROR: control reached end of function without RETURN``
I tried RETURN QUERY EXECUTE format('... but I get a ERROR: cannot use RETURN QUERY in a non-SETOF function
AS #dezso mentioned, you'll need dynamic SQL in this case.
Dynamic SQL with EXECUTE
So, you're on the right track; forming a dynamic SQL statement using PL/pgSQL, but you just need the finishing touches:
CREATE or REPLACE FUNCTION convert_from_lon_lat(long float, lat float, _table text)
RETURNS integer AS $$
BEGIN
RETURN QUERY EXECUTE format('SELECT id FROM %I AS vertices
ORDER BY vertices.geom <->ST_SetSrid(ST_MakePoint(long,lat),4326) LIMIT 1;',_table);
END
$$ LANGUAGE plpgsql;
I believe this should solve your issues.
Note: We've discovered an error with the above solution and using SETOF, I've attempted to correct the issues below.
EDIT:
A few edits here, hopefully one solution will fix your issue. Also, please excuse any syntax errors in my previous & current solutions; I don't have time to test them right now. :(
1) You could just try returning a SETOF integers, knowing that of course you'll only return the one. Your return type in this case will then be a single, one-column row containing an integer.
CREATE or REPLACE FUNCTION convert_from_lon_lat(long float, lat float, _table text)
RETURNS SETOF integer AS $$
BEGIN
RETURN QUERY EXECUTE format('SELECT id FROM %I AS vertices
ORDER BY vertices.geom <->ST_SetSrid(ST_MakePoint(long,lat),4326) LIMIT 1;',_table);
END
$$ LANGUAGE plpgsql;
and then call as:
SELECT * FROM convert_from_lon_lat(...);
2) To specifically return an integer, I think you can try this:
CREATE or REPLACE FUNCTION convert_from_lon_lat(long float, lat float, _table text)
RETURNS integer AS $$
DECLARE
return_id integer;
BEGIN
EXECUTE format('SELECT id FROM %I AS vertices
ORDER BY vertices.geom <->ST_SetSrid(ST_MakePoint(long,lat),4326) LIMIT 1;',_table)
INTO return_id;
RETURN return_id;
END
$$ LANGUAGE plpgsql;
I'm trying to write a simple postgres function which looks more or less like that:
CREATE OR REPLACE FUNCTION USER_TOTALS(column_name varchar, in_t users) RETURNS float AS $$
DECLARE
sum float;
BEGIN
sum = (SELECT SUM($1) FROM jobs WHERE jobs.technician_id = in_t.id);
RETURN sum;
END;
$$ LANGUAGE plpgsql;
And i need to use it like that:
SELECT users.*, USER_TOTALS('jobs.price', users.*) AS total_price_value FROM users;
Hovewer, that's obviously not working cause SUM() function expects to get a column name but my code passes a varchar to it, so the error says:
Function sum(character varying) does not exist
The question is - can i somehow cast a varchar variable to column name var type? I've been googling for this thing for about 2 hours now and i have no idea how can i make that happen.
A recommended form:
CREATE OR REPLACE FUNCTION USER_TOTALS(column_name varchar, in_t users)
RETURNS float AS $$
DECLARE
sum float;
BEGIN
EXECUTE format('SELECT SUM(%I) FROM jobs WHERE jobs.technician_id=$1', column_name)
INTO sum
USING in_t;
RETURN sum;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION USER_TOTALS(column_name varchar, in_t users) RETURNS float AS $$
DECLARE
sum float;
BEGIN
EXECUTE 'SELECT SUM('||column_name||') FROM jobs WHERE jobs.technician_id='||in_t INTO sum;
RETURN sum;
END;
$$ LANGUAGE plpgsql;