Using extract function in your own sql function - postgresql

I'm trying to create the following function into my Postgres database:
create or replace function public.extract_hour(time_param timestamp with time zone) returns double precision language sql as $function$
SELECT EXTRACT(hour from timestamp time_param);
$function$;
but I get the following error:
ERROR: syntax error at or near "time_param"
I tried to put instead of time_param $0, but the same error occur. Can somebody explain me how to solve that?

Obviously, you are using an older version of PostgreSQL (which you neglected to provide). Referring to parameter names is possible since PostgreSQL 9.2. In older versions you can only refer to positional parameters:
CREATE OR REPLACE FUNCTION public.extract_hour(time_param timestamptz)
RETURNS double precision LANGUAGE sql AS
$function$
SELECT EXTRACT(hour from $1);
$function$;
Also I removed the misplaced keyword timestamp.
While referring to parameter names has been possible in PL/pgSQL functions for a long time now.
We fixed a documentation glitch to the opposite just recently.

Related

Implicitly cast an ISO8601 string to TIMESTAMPTZ (postgresql) for Debezium

I am using a 3rd party application (Debezium Connector). It has to write date time strings in ISO-8601 format into a TIMESTAMPTZ column. Unfortunately this fails, because there is no implicit cast from varchar to timestamp tz.
I did notice that the following works:
SELECT TIMESTAMPTZ('2021-01-05T05:17:46Z');
SELECT TIMESTAMPTZ('2021-01-05T05:17:46.123Z');
I tried the following:
Create a function and a cast
CREATE OR REPLACE FUNCTION varchar_to_timestamptz(val VARCHAR)
RETURNS timestamptz AS $$
SELECT TIMESTAMPTZ(val) INTO tstz;
$$ LANGUAGE SQL;
CREATE CAST (varchar as timestamptz) WITH FUNCTION varchar_to_timestamptz (varchar) AS IMPLICIT;
Unfortunately, it gives the following errors:
function timestamptz(character varying) does not exist
I also tried the same as above but using plpgsql and got the same error.
I tried writing a manual parse, but had issues with the optional microsecond segment which gave me the following
CREATE OR REPLACE FUNCTION varchar_to_timestamptz (val varchar) RETURNS timestamptz AS $$
SELECT CASE
WHEN $1 LIKE '%.%'
THEN to_timestamp($1, 'YYYY-MM-DD"T"HH24:MI:SS.USZ')::timestamp without time zone at time zone 'Etc/UTC'
ELSE to_timestamp($1, 'YYYY-MM-DD"T"HH24:MI:SSZ')::timestamp without time zone at time zone 'Etc/UTC' END $$ LANGUAGE SQL;
Which worked, but didn't feel correct.
Is there a better way to approach this implicit cast?
If the value should be converted upon insert, define an assignment cast. You need no function; using the type input and output functions will do:
CREATE CAST (varchar AS timestamptz) WITH INOUT AS ASSIGNMENT;
Be warned that messing with the casts on standard data types can lead to problems, because it increases the ambiguity. It would be much better if you could find a way to use an explicit cast.

PL/PostgreSQL how to convert a variable into a table name

So I have a function in PostgreSQL that dynamically selects columns from a dynamic table. I got this solution from this post and it works great other than one thing.
This is inside of a file that is connected to a Node server, and so the $1 and $2 in the second SELECT * FROM represent values passed from there. The issue right now is that I am getting a syntax error that I don't understand (I am newer to SQL so that may be why).
$2 represents the name of the table to be selected from as a string, so for example it could be 'goals'. The error is syntax error at or near "'goals'". I realize that it cannot be a string with single quotes (I believe) and so I am wondering how to convert that variable to be a table name? using "goals" there as well as goals, for example works as expected, though I'm not sure how to do that outside of a function.
CREATE OR REPLACE FUNCTION get_data(user_id INT, table_name anyelement)
RETURNS SETOF ANYELEMENT AS $$
BEGIN
RETURN QUERY EXECUTE
format('SELECT * FROM %s WHERE user_id = $1', pg_typeof(table_name)) USING user_id;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM get_data($1, NULL::$2);
$1 is 5 and $2 is 'goals' for example
After many hours of trying to figure it out, thanks to Adrian's comment, I found MassiveJS (how I'm connecting to my PostgreSQL server) has inline functions to do queries. In my controller file in my server I was able to create a one line function as such:
const data = await db[tableName].where("user_id=$1", [userId])
Didn't know inline SQL existed in MassiveJS, so that was great to find out!

How to create a new date range type with included upper bound in Postgres?

Postgres comes with a nice feature called Range Types that provides useful range functionality (overlaps, contains, etc).
I am looking to use the daterange type, however I think the type was implemented with an awkward choice: the upper bound of the daterange is excluded. That means that if I defined my value as 2014/01/01 - 2014/01/31, this is displayed as [2014/01/01, 2014/01/31) and the 31st of January is excluded from the range!
I think this was the wrong default choice here. I cannot think of any application or reference in real life that assumes that the end date of a date range is excluded. At least not to my experience.
I want to implement a range type for dates with both lower and upper bounds included, but I am hitting the Postgres documentation wall: References on how to create a new discrete range type are cryptic and lack any examples (taken from the documentation: Creating a canonical function is a bit tricky, since it must be defined before the range type can be declared).
Can someone provide some help on this? Or even directly the implementation itself; it should be 5-10 lines of code, but putting these 5-10 lines together is a serious research effort.
EDIT: Clarification: I am looking for information on how to create the proper type so that inserting [2014/01/01, 2014/01/31] results in a upper(daterange) = '2014/01/31'. With the existing daterange type this value is "converted" to a [2014/01/01, 2014/02/01) and gives a upper(daterange) = '2014/02/01'
Notice the third constructor parameter:
select daterange('2014/01/01', '2014/01/31', '[]');
daterange
-------------------------
[2014-01-01,2014-02-01)
Or a direct cast with the upper bound included:
select '[2014/01/01, 2014/01/31]'::daterange;
daterange
-------------------------
[2014-01-01,2014-02-01)
Edit
Not a new type (wrong approach IMHO) but a proper function:
create function inclusive_upper_daterange(dtr daterange)
returns date as $$
select upper(dtr) - 1;
$$ language sql immutable;
select inclusive_upper_daterange('[2014/01/01, 2014/01/31]'::daterange);
inclusive_upper_daterange
---------------------------
2014-01-31
Following the instructions on Postgres documentation I came up with the following code to create the type I need. However it won't work (read on).
CREATE TYPE daterange_;
CREATE FUNCTION date_minus(date1 date, date2 date) RETURNS float AS $$
SELECT cast(date1 - date2 as float);
$$ LANGUAGE sql immutable;
CREATE FUNCTION dr_canonical(dr daterange_) RETURNS daterange_ AS $$
BEGIN
IF NOT lower_inc(dr) THEN
dr := daterange_(lower(dr) + 1, upper(dr), '[]');
END IF;
IF NOT upper_inc(dr) THEN
dr := daterange_(lower(dr), upper(dr) - 1, '[]');
END IF;
RETURN dr;
END;
$$ LANGUAGE plpgsql;
CREATE TYPE daterange_ AS RANGE (
SUBTYPE = date,
SUBTYPE_DIFF = date_minus,
CANONICAL = dr_canonical
);
As far as I can tell, this definition follows the specification exactly. However it fails at declaring the dr_canonical function with ERROR: SQL function cannot accept shell type daterange_.
It looks like (code also) it is impossible to declare a canonical function using any language other than C! So it is practically impossible to declare a new discrete range type, especially if you use a Postgres cloud service that gives no access to the machine running it. Well played Postgres.
Using PostgresSQL 11 you can solve presentation part using upper_inc function, example:
select
WHEN upper_inc(mydaterange) THEN upper(mydaterange)
ELSE date(upper(mydaterange)- INTERVAL '1 day')
END
I managed to create a custom type for the date range:
CREATE or replace FUNCTION to_timestamptz(arg1 timestamptz, arg2 timestamptz) RETURNS float8 AS
'select extract(epoch from (arg2 - arg1));' LANGUAGE sql STRICT
IMMUTABLE;
;
create type tsrangetz AS RANGE
(
subtype = timestamptz,
subtype_diff =
to_timestamptz
)
;
select tsrangetz(current_date, current_date + 1)
--["2020-10-05 00:00:00+07","2020-10-06 00:00:00+07")
;

Retrieving a value from a RECORD

In a plpgsql function, I have a variable of type record:
my_rec RECORD;
This record contains a row from an arbitrary table, so I do not know the columns before it is executed.
However, I do have the name of at least one of the columns available as a varchar.
The question is: How do I retrieve the value for a given column from my_rec?
Use hstore to work with records with dynamic columns in PL/PgSQL functions:
CREATE EXTENSION hstore;
CREATE OR REPLACE FUNCTION test_fn(col_name text) RETURNS text AS $$
DECLARE
input_row record;
col_value text;
BEGIN
SELECT INTO input_row
*
FROM ( VALUES ('a','b','c',1) ) AS dummyrow(col1,col2,col3,intcol);
SELECT INTO col_value
hstore(input_row) -> col_name;
RETURN col_value;
END;
$$ LANGUAGE 'plpgsql';
hstore is an extension, but it's an extension that's been bundled with PostgreSQL since 8.3 and has been installable using CREATE EXTENSION since 9.1. The record-to-hstore conversion has been supported since something like 8.4 or 9.0.
I don't know of a way to do this in plpgsql. I did a bit of testing for you and tried to make a "EXECUTE SELECT" solution work, such as:
EXECUTE 'select $1.' || quote_ident(the_param) USING my_rec INTO my_var;
This does not work for me and I get:
could not identify column "{{param_value here}}" in record data type
Here is a very similar question from a few years ago saying that it is not possible with plpgsql. Per it's suggestion, it appears that it should be possible with some other languages. Quoting Tom Lane's answer:
There is no way to do that in plpgsql. You could do it in the other PLs
(eg plperl, pltcl) since they are not as strongly typed as plpgsql.

Not able to run block in PostgreSQL 8.2

I can't run this block in PostgreSQL 8.2.
DECLARE
curtime char;
BEGIN
curtime := 'now';
INSERT INTO logtable VALUES (logtxt, curtime);
RETURN curtime;
END;
When I try it shows the error:
ERROR: syntax error at or near "char"
SQL state: 42601
It sounds like you're trying to run a PL/PgSQL code block stand-alone, without wrapping it up in a function using CREATE OR REPLACE FUNCTION. That won't work, you need to include it in a function or (from PostgreSQL 9.0) a DO block. PL/PgSQL and plain SQL are different languages so you can't just run PL/PgSQL code directly.
It'd help if you explained why you're trying to write the code you pasted. I suspect you're trying to solve a problem that's better handled with a trigger function like an audit trigger.
Some important notes:
You need to update PostgreSQL: PostgreSQL 8.2 is dangerously out of date and unsupported. security and bug fixes are no longer being released. Upgrade urgently to a supported version, but make sure to read the release notes for each major ".0" version like "8.3.0", "8.4.0", etc for migration and compatibility advice.
Avoid 'now': Also, instead of using 'now' you should usually use the current date/time functions, particularly current_timestamp.
current_timestamp is stable: The hoop-jumping you are doing is probably unnecessary because the value of current_timestamp (and 'now'::timestamp) doesn't change for the duration of a transaction. Eg:
regress=# BEGIN;
regress=# SELECT current_timestamp;
2012-08-14 14:52:43.382596+08
regress=# SELECT pg_sleep(5);
regress=# SELECT current_timestamp;
2012-08-14 14:52:43.382596+08
Details
Your intention appears to be something like the following (incorrect, do not use) code:
CREATE OR REPLACE FUNCTION some_function(logtxt text) RETURNS timestamptz AS $$
DECLARE
curtime char;
BEGIN
curtime := 'now';
INSERT INTO logtable VALUES (logtxt, curtime);
RETURN curtime;
END;
$$ LANGUAGE 'plpgsql';
but you've misused the char datatype, which requires a length parameter. It defaults to 1 if not supplied so you'll get:
regress=# SELECT some_function();
ERROR: value too long for type character(1)
CONTEXT: PL/pgSQL function "some_function" line 5 at assignment
NEVER use the char datatype in SQL; use varchar or text. For cross-database portability varchar(n) where n is a maximum length is required; if portability isn't needed use text.
If you change char to text in the above, your code might run, but it still doesn't make any sense. I strongly suspect that you really want to write:
CREATE OR REPLACE FUNCTION some_function(logtxt text) RETURNS timestamptz AS $$
BEGIN
INSERT INTO logtable VALUES (logtxt, current_timestamp);
RETURN current_timestamp;
END;
$$ LANGUAGE 'plpgsql';
... but you didn't know about the current date/time functions.
Even that's too much, really. I think you're trying to solve a problem that's a better fit for a trigger.