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

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.

Related

Error calling procedure in posgres "No procedure matches the given name and argument types. You might need to add explicit type casts."

CREATE or replace PROCEDURE mytransactions (n_transactions_id VARCHAR,
n_transaction_amount SMALLINT,
n_transaction_date TIMESTAMP,
n_Delivery_date Date,
n_customer_id VARCHAR,
n_product_id VARCHAR,
n_store_id VARCHAR)
LANGUAGE plpgsql AS
$BODY$
BEGIN
INSERT INTO transactions
(transactions_id,
transaction_amount,
transaction_date,
Delivery_date,
customer_id,
product_id,
store_id)
VALUES
(n_transactions_id, n_transaction_amount,
n_transaction_date,
n_Delivery_date,
n_customer_id,
n_product_id,
n_store_id);
END;
$BODY$
Here is my stored procedure, it creates successfully, however once I call
CALL mytransactions
('555', 3, current_timestamp , to_date('2022-10-25','YYYY-MM-DD'),
'003', '300', '002RW');
it I get an error.
ERROR: procedure mytransactions(unknown, integer, timestamp with time zone, date, unknown, unknown, unknown) does not exist
LINE 1: CALL mytransactions
^
HINT: No procedure matches the given name and argument types. You might need to add explicit type casts.
Here you can find full tables https://dbfiddle.uk/9_NIQDw6
You need to typecast the parameters. This will work, allthough you'll get other errors when the procedure does it's things.
CALL mytransactions
('555'::varchar, 3::smallint, current_timestamp::timestamp , to_date('2022-10-25','YYYY-MM-DD'),
'003'::varchar, '300'::varchar, '002RW'::varchar);
The issue is with your second and third arguments. As you can see from the error, Postgres is assuming the second argument (3) is an integer, not smallint and the third argument is created using current_timestamp, which returns a timestamp with time zone, not a timestamp.
You can fix the smallint issue by simply casting to a smallint. For the timestamp, you'll need to figure out what value you actually want in there. I would recommend using timestamp with time zone everywhere, if possible.
Here's an example of calling your procedure that will work, but the timestamp may not be what you actually want:
CALL mytransactions
('555', 3::smallint, current_timestamp::timestamp , to_date('2022-10-25','YYYY-MM-DD'),
'003', '300', '002RW');
Be careful with that conversion to timestamp as the result will depend on the timezone settings of the SQL client.
Your argument data types don't match the parameter data types, and there are no implicit type casts that can be applied. For example, 3 is an integer, which cannot be cast to smallint implicitly. You'd have to use an explicit type cast like CAST (3 AS smallint).
To avoid that problem, it is a good practice to use preferred data types for function parameters. Each class of data type has one of them. For string types, it is text, for numbers numeric and for date/time it is timestamp with time zone. Then the type resolution rules will usually work the way you want.

Creating insert function with TIMESTAMP

I created simple table with a simple function, to insert some logs for the elapsed semester:
CREATE TABLE log_elapsedsemester(
sy char(9) NOT NULL,
sem char(1) NOT NULL,
date_recorded TIMESTAMP NOT NULL,
recordedby varchar(255)
);
CREATE OR REPLACE FUNCTION addelapsedsemester(p_sy char,p_sem char,p_date_recorded
TIMESTAMP,p_recordedby varchar)
returns void
AS
$$
BEGIN
insert into log_elapsedsemester (sy,sem,date_recorded,recordedby) values
(p_sy,p_sem,p_date_recorded,p_recordedby);
END
$$
LANGUAGE plpgsql;
But evertime I use
select addelapsedsemester('2019-2020','1',now(),'sample#gmail.com');
I get the error:
No function matches the given name and argument types. You might need to add explicit type casts.
If I use a simple INSERT with no function it inserts successfully:
insert into log_elapsedsemester(sy,sem,date_recorded,recordedby) values ('2020-
2021','1',now(),'sample#gmail.com');
I'm using PostgreSQL 9.5 with pgadmin III.
You need to cast to timestamp explicitly. Like:
SELECT addelapsedsemester('2019-2020', '1', now()::timestamp,'sample#gmail.com');
Or use LOCALTIMESTAMP instead of now()::timestamp (equivalent).
The function now() returns type timestamp with time zone (timestamptz), while your function takes timestamp without time zone (timestamp). The now() function produces a typed value (unlike the other untyped literals), where Postgres is more hesitant to coerce it to a different type. Function type resolution does not succeed.
The same type coercion still works for the bare INSERT command because (quoting the manual):
If the expression for any column is not of the correct data type, automatic type conversion will be attempted.
Be aware that the cast from timestamptz to timestamp depends on the current timezone setting of the session. You may want to be more explicit. Like now() AT TIME ZONE 'Europe/London'. Or use timestamptz to begin with. Then your original call without cast just works. See:
Now() without timezone
Also, you most probably do not want to use the type char, which is misleading short syntax for character(1). Use text or varchar instead. See:
Any downsides of using data type "text" for storing strings?
This table definition would make more sense:
CREATE TABLE log_elapsedsemester(
sy varchar(9) NOT NULL
, sem integer NOT NULL
, date_recorded timestamptz NOT NULL
, recordedby text
);
Or even:
sy integer NOT NULL -- 2019 can stand for '2019-2020'
Function parameters would match the column type.

Does pg_typeof ever return alternative type names?

For context:
I have a sql function that takes an array of strings. Those strings are sql expressions that are stored in a table and used some time later for dynamically creating some queries.
I want to restrict the data types of those expressions to some limited set. For that I intend to evaluate the expressions and check the data type with pg_typeof like so:
create function fun(expressions text[]) returns void as $$
declare
expression_type text;
begin
for i in 1..array_length(expressions, 1) loop
execute format('select pg_typeof((select %s from some_table where false))', expressions[i]) into expression_type ;
-- check that expression_type has legal value, raise exception otherwise
end loop;
-- store expressions for later use
end;
$$ language plpgsql;
For example suppose that integer and timestamp without time zone are the allowed types.
I'd like to list the allowed types in an enum:
create type supported_types as enum ('integer', 'timestamp with time zone');
For some types PostgreSQL documentatsion also mentions alternative names, e.g int4 instead of integer, timestamp instead of timestamp without time zone etc.
My queston is that do I have to worry about these "alternative names" for types when I enumerate the ones I care about?
I.e if I include integer do I also have to include int4 or in other words does pg_typeof ever return int4 instead of integer (or timestamp instead of timestamp without time zone etc)?
It does not.
Function internally (C code) returns OID, but since in PostgreSQL it is declared as returning regtype, it is cast to it. Since OID is the same regardless of name/alias for type, it will always give the same result.
postgres=# SELECT 'int4'::regtype::oid::regtype;
regtype
---------
integer
(1 row)

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")
;

PostgreSQL create index on cast from string to date

I'm trying to create an index on the cast of a varchar column to date. I'm doing something like this:
CREATE INDEX date_index ON table_name (CAST(varchar_column AS DATE));
I'm getting the error: functions in index expression must be marked IMMUTABLE But I don't get why, the cast to date doesn't depends on the timezone or something like that (which makes a cast to timestamp with time zone give this error).
Any help?
Your first error was to store a date as a varchar column. You should not do that.
The proper fix for your problem is to convert the column to a real date column.
Now I'm pretty sure the answer to that statement is "I didn't design the database and I cannot change it", so here is a workaround:
CAST and to_char() are not immutable because they can return different values for the same input value depending on the current session's settings.
If you know you have a consistent format of all values in the table (which - if you had - would mean you can convert the column to a real date column) then you can create your own function that converts a varchar to a date and is marked as immutable.
create or replace function fix_bad_datatype(the_date varchar)
returns date
language sql
immutable
as
$body$
select to_date(the_date, 'yyyy-mm-dd');
$body$
ROWS 1
/
With that definition you can create an index on the expression:
CREATE INDEX date_index ON table_name (fix_bad_datatype(varchar_column));
But you have to use exactly that function call in your query so that Postgres uses it:
select *
from foo
where fix_bad_datatype(varchar_column) < current_date;
Note that this approach will fail badly if you have just one "illegal" value in your varchar column. The only sensible solution is to store dates as dates,
Please provide the database version, table ddl, and some example data.
Would making your own immutable function do what you want, like this? Also look into creating a new cast in the docs and see if that does anything for you.
create table emp2 (emp2_id integer, hire_date VARCHAR(100));
insert into emp2(hire_date)
select now();
select cast(hire_date as DATE)
from emp2
CREATE FUNCTION my_date_cast(VARCHAR) RETURNS DATE
AS 'select cast($1 as DATE)'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
CREATE INDEX idx_emp2_hire_date ON emp2 (my_date_cast(hire_date));