Search code examples
sqlpostgresqlcastingcomposite

casting from and to composite types in postgres


I have created a composite type in postgres:

CREATE TYPE mytimestamp AS (t timestamp with time zone, o int);

and added it to a table

CREATE TABLE t (t0 mytimestamp)

Using a custom conversion function and the following CAST, I was able to convert a simple timestamp with time zone into a 'mytimestamp' in an INSERT statement:

CREATE CAST (timestamp with time zone AS mytimestamp) 
WITH FUNCTION to_mytimestamp(timestamp with time zone) AS ASSIGNMENT;

INSERT INTO t (t0) VALUES(now()); -- works as intended

However, I cannot seem to create a CAST that works with the following statement:

CREATE CAST (varchar AS mytimestamp) 
WITH FUNCTION to_mytimestamp(varchar) AS ASSIGNMENT;

INSERT INTO t (t0) 
VALUES('2014-09-11 13:30:12.564+02'); -- returns 'malformed record literal'

(I'd need a conversion from varchar, char or text - but simply implementing the conversion function and adding the respective CAST doesn't seem to work)

I know I could achieve this by altering the query to

INSERT INTO t (t0) 
VALUES('2014-09-11 13:30:12.564+02'::varchar); -- works
-- the to_mytimestamp(varchar) function is called

However in my case, I can't change the queries since they're defined in some code that shouldn't be changed anymore.

Is there a way for me to ensure the to_mytimestamp(varchar) function is called when executing the above INSERT statement (without having to append ::varchar)?

Thanks a bunch!


edit: Here are the mentioned functions (replace text with varchar or char in to_mytimestamp() as needed):

CREATE OR REPLACE FUNCTION to_mytimestamp(t text)
  RETURNS mytimestamp AS
$BODY$
declare
ts mytimestamp;
offs text;
matches text[];
begin

 offs = substr(t, length(t) - 5, 6);
 matches = regexp_matches(offs, '[+-][0-9.:]*', 'g');
 offs = split_part(matches[1], ':', 1)::integer * 3600 + split_part(matches[1], ':', 2)::integer * 60;
 ts.t = t::timestamp with time zone;
 ts.o = offs;
 return ts;

end
$BODY$
  LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION to_mytimestamp(t timestamp with time zone)
  RETURNS mytimestamp AS
' select $1, extract(timezone from $1) '
  LANGUAGE sql;

explanation: the functions extract the utc offset and store it in a separate value for later reconstruction. note: to_mytimestamp(t timestamp with time zone) stores the system offset, not the one one would assume is stored in the argument t, since timestamp with time zone doesn't actually contain offset data - but this is irrelevant to the problem :)


Solution

  • I don't think you can do this.

    When you write a string literal 'foo' in postgres, it actually has an "unknown" type, rather than text. Thus, your cast function does not match because the types differ. This is why you can do:

    SELECT '2014-09-11 13:30:12.564+02'::text::mytimestamp;
    

    and it will work as expected.

    Even if you write a version of the function that handles unknown as the parameter type (and converts this to text first), and create the relevant cast:

    CREATE CAST (unknown AS mytimestamp) 
    WITH FUNCTION mytimestamp(unknown) 
    AS IMPLICIT;
    

    Postgres will still try to use the original composite type conversion function. Indeed, this breaks the "convert-to-text-first" path, because there is no better match for mytimestamp(text), so it uses the in-built function.