Postgres Cast Composite Types - postgresql

I'm facing a strange behaviour when using a function to cast to composite type in Postgres 9.6.
I have declared a composite type "vector" as x,y,z - each of double precision as well as a cast as follows:
create type vector as(
x double precision,
y double precision,
z double precision
);
create cast (text as vector)
with function vector_from_text(text) as implicit;
Function vector_from_text looks like this:
create or replace function vector_from_text(text, out result vector) strict immutable language plpgsql as $$
declare
d double precision[];
begin
result := null;
if ($1 is null) then
return;
end if;
begin
with c1 as (
select $1::jsonb src
)
select row((src->>'x')::double precision, (src->>'y')::double precision, (src->>'z')::double precision)::vector
**into result** -- see below
from c1;
exception
when others then
d := translate($1, '()', '{}')::double precision[];
result := row(d[1], d[2], d[3])::vector;
end;
end$$
;
The function returns null on null, or a vector type for both input formats either a json-string like '{"x":0, "y":0, "z":0}' or a constructor expression like '(0,0,0)'.
The Problem:
For json-like inputs the functions returns the error
invalid input syntax for type double precision: "(0,0,0)"
as soon the select statement contains the line into result. If I remove this line or change the output variable from result to something of type text the functions works as expected.
Why is it not possible to assign an already to type vector casted value into a vector? Don't get it!

You do not need to (and in fact should not) create a cast from text. When you create a composite type you can cast a text to the type without any additional steps:
create type my_record as(
x int,
y int,
z int
);
select '(1,2,3)'::my_record;
my_record
-----------
(1,2,3)
(1 row)
If you want to use jsonb, create a cast from jsonb to the type:
create or replace function my_record_from_jsonb(obj jsonb, out result my_record)
strict immutable language plpgsql as $$
begin
select (obj->>'x')::int, (obj->>'y')::int, (obj->>'z')::int
into result;
end
$$;
create cast (jsonb as my_record)
with function my_record_from_jsonb(jsonb);
select '{"x":1, "y":2, "z":3}'::jsonb::my_record;
my_record
-----------
(1,2,3)
(1 row)
Do not try to interpret text literals in two different ways. If you want to use jsonb syntax, use jsonb type. Creating a custom implicit cast from text is particularly unreasonable. Read in the documentation::
It is wise to be conservative about marking casts as implicit. An overabundance of implicit casting paths can cause PostgreSQL to choose surprising interpretations of commands, or to be unable to resolve commands at all because there are multiple possible interpretations. A good rule of thumb is to make a cast implicitly invokable only for information-preserving transformations between types in the same general type category. For example, the cast from int2 to int4 can reasonably be implicit, but the cast from float8 to int4 should probably be assignment-only. Cross-type-category casts, such as text to int4, are best made explicit-only.

Related

Casting element to user defined type in SQL functions in PostgreSQL

Say, I have composite type id_and_name.
CREATE TYPE id_and_name AS (id integer, name text);
I want to use this (id, name) structure as parameter in function, but I want that function user wouldn't need to cast manually: SELECT fun((1, 'name')::id_and_name); user should be good with SELECT fun((1, 'name')). That means that my function parameter will be anyelement or similar and I should cast it to id_and_name in function:
CREATE OR REPLACE FUNCTION fun(anyelement)
RETURNS id_and_name AS $$
SELECT $1::id_and_name;
$$ LANGUAGE sql;
For some reason SELECT fun((1, 'name')) gives me error:
ERROR: cannot cast type record to id_and_name
LINE 2: SELECT $1::id_and_name;
^
While the same query from function with substituted parameter works fine:
SELECT (1, 'name')::id_and_name;
How should I do such casting in functions?
I'm on PostgreSQL 10
As mentioned in the comments, it is preferable to specify argument. Otherwise you may use row_to_json to extract individual record elements and cast them
CREATE OR REPLACE FUNCTION fun(anyelement)
RETURNS id_and_name AS $$
SELECT (j->>'f1',j->>'f2')::id_and_name from row_to_json($1) as j;
$$ LANGUAGE sql;
DEMO

How to cast earth datatype to json in Postgresql

I want to cast the earth datatype to a json array in Postgresql (9.5).
This is what I have:
CREATE OR REPLACE FUNCTION earth_to_json (inpt earth)
RETURNS json AS $$
SELECT json_build_array(latitude(inpt), longitude(inpt))
$$ LANGUAGE SQL;
CREATE CAST (earth AS JSON) WITH FUNCTION earth_to_json(earth) AS IMPLICIT;
When executing the query above, I get the following output:
WARNING: cast will be ignored because the source data type is a domain
CREATE CAST
Query returned successfully in 47 msec.
So the earth datatype is domain, according to the message above. I know earthdistance is based on the cube module, but I assumed the earth datatype is a real datatype so I should be able to cast.
So if above code should work, the code below should run fine.
SELECT ll_to_earth(10.10, 30.01)::JSON
But I get the error
ERROR: cannot cast type earth to json LINE 1
What I found out:
The earth datatype is not a real datatype. This is the type cube, with constraints (constraints are defined in the earth-domain). So I really need to create a cast which casts the cube datatype to json.
When you accidentally have a cube with fits in the earth domain, it will be returned as lat, lon array. You could create your own datatype to work around this. For me this works ok, as I do not use the cube datatype.
CREATE OR REPLACE FUNCTION cube_to_json (inpt cube)
RETURNS json AS $$
BEGIN
BEGIN
RETURN json_build_array(latitude(inpt), longitude(inpt));
EXCEPTION WHEN SQLSTATE '23514' THEN
RETURN CASE WHEN json_array_length(sub) = 1 THEN sub->0 ELSE sub END FROM (SELECT translate('[' || inpt::TEXT || ']', '()', '[]')::JSON AS sub) AS o;
END;
END;
$$ LANGUAGE plpgsql;
CREATE CAST (cube AS json) WITH FUNCTION cube_to_json(cube) AS IMPLICIT
-- Now test
SELECT (ll_to_earth(10,20))::JSON; -- Outputs [10,20]
SELECT ('(5437508.36471516, 3140612.41127573), (118.44062468, 1)'::CUBE)::JSON; -- Outputs "[[5437508.36471516, 3140612.41127573], [118.44062468, 1]]"

How to pass multiple rows to PostgreSQL function?

How can we pass an array of (an unlimited amount of) rows (ie, a constant table) as the parameter/argument of a PostgreSQL function?
Here's an idea:
CREATE TYPE foo AS (
x bigint,
y smallint,
z varchar(64)
);
CREATE OR REPLACE FUNCTION bar(bigint, foo[]) RETURNS TABLE(a bigint, x bigint, y smallint, z varchar(64)) AS
$$
SELECT $1, x, y, z FROM unnest($2);
$$
LANGUAGE SQL;
The below function call works, but is there a way to make it shorter?
SELECT * FROM bar(1, ARRAY[(1,2,'body1'),(2,1,'body2')]::foo[]);
For example, we can't remove the ::foo[] cast, but is there a way to rewrite things so that we can omit it?
Should we be using a variatic argument?
My Google searches kept leading me here, so I'm going to post an answer that may not match exactly the needs of the OP, but might be helpful to others who see the title How to pass multiple rows to PostgreSQL function?
The OPs original request was for a type:
CREATE TYPE foo AS (
x bigint,
y smallint,
z varchar(64)
);
If you are like me, you may want to pass in the results of a standard SELECT query to a function. So imagine I have a table (rather than a type) created as:
CREATE TABLE foo AS (
x bigint,
y smallint,
z varchar(64)
);
I want to pass to a function the results of:
SELECT * from foo WHERE x = 12345;
The results may be zero or many rows.
According to the postgres docs at https://www.postgresql.org/docs/9.5/static/rowtypes.html creating a table also leads to the creation of a composite type with the same name. Which is helpful, since this automatically handles the CREATE TYPE foo in the original question, which I can now pass in to a function as an array.
Now I can create a function that accepts an array of foo typed values (simplified to focus on what is passed in, and how the records are used, rather than what is returned):
CREATE OR REPLACE FUNCTION bar(someint bigint, foos foo[]) RETURNS ...
LANGUAGE plpgsql
AS $$
DECLARE
foo_record record;
begin
-- We are going to loop through each composite type value in the array
-- The elements of the composite value are referenced just like
-- the columns in the original table row
FOREACH foo_record IN ARRAY foos LOOP
-- do something, maybe like:
INSERT INTO new_foo (
x, y, z
)
VALUES (
foo_record.x,
foo_record.y,
foo_record.z
);
END LOOP;
RETURN...
END;
$$;
This function bar(bigint, foo[]) can then be called quite simply with:
SELECT bar(4126521, ARRAY(SELECT * from foo WHERE x = 12345));
which passes in all the rows of a query on the foo table as a foo typed array. The function as we have seen then performs some action against each of those rows.
Although the example is contrived, and perhaps not exactly what the OP was asking, it fits the title of the question and might save others from having to search more to find what they need.
EDIT naming the function arguments makes things easier
PostgreSQL doesn't have table-valued variables (yet), so nothing's going to be pretty. Passing arrays is inefficient but will work for reasonable-sized inputs.
For bigger inputs, what often works is to pass a refcursor. It's clumsy, but can be practical for larger data sets, sometimes combined with temp tables.
e.g.
CREATE OR REPLACE FUNCTION bar(i bigint, c refcursor) RETURNS TABLE(a bigint, x bigint, y smallint, z varchar(64)) AS
$$
DECLARE
cursrow foo;
BEGIN
LOOP
FETCH NEXT FROM c INTO cursrow;
a := i;
x := cursrow.x;
y := cursrow.y;
z := cursrow.z;
RETURN NEXT;
IF NOT FOUND THEN
EXIT;
END IF;
END LOOP;
RETURN;
END;
$$;
usage:
demo=> BEGIN;
BEGIN
demo=> DECLARE "curs1" CURSOR FOR VALUES (1,2,'body1'), (2,1,'body2');
DECLARE CURSOR
craig=> SELECT bar(1, 'curs1');
bar
---------------
(1,1,2,body1)
(1,2,1,body2)
(1,,,)
(3 rows)
demo=> COMMIT;
COMMIT
Not beautiful. But then, plpgsql never is. It's a pity it doesn't have row-valued lvalues, as being able to write something like (x, y, z) := cursrow or ROW(x, y, z) := cursrow would make it a bit less ugly.
RETURN NEXT works, but only if you return record not named out parameters or TABLE.
And sadly, you can't use SQL (not plpgsql) FETCH ALL in a subexpression so you cannot write
RETURN QUERY NEXT i, cursrow.* FROM (FETCH ALL FROM c) AS cursrow;
It seems that one of the problems is the using of smallint type which can not be converted implicitly from an int constants. And consider the following:
-- drop function if exists bar(bigint, variadic foo[]);
-- drop type if exists foo;
CREATE TYPE foo AS (
x bigint,
y int, -- change type to integer
z varchar(64)
);
CREATE OR REPLACE FUNCTION bar(bigint, variadic foo[]) RETURNS TABLE(
a bigint,
x bigint,
y int, -- and here
z varchar(64)) AS
$$
SELECT $1, x, y, z FROM unnest($2);
$$
LANGUAGE SQL;
-- Voila! It is even simpler then the using of the ARRAY constructor
SELECT * FROM bar(1, (1,2,'body1'), (2,1,'body2'), (3,4,'taddy bear'));
dbfiddle
About variadic parameters

Are there some "direct casts" for JSONB parameters?

This is a typical wrap function (overloading a non-json foo() function) for use in JSONB context,
-- suppose FUNCTION foo(text[], numeric, boolean) returns int.
CREATE FUNCTION foo(JSONB) RETURNS int AS $f$
SELECT foo(
(SELECT array_agg(x) FROM jsonb_array_elements_text($1->'list') t(x) ),
($1->>'lim')::numeric, -- why cast JSONB number to numeric?
($1->>'osort')::boolean -- idem for boolean
);
$f$ LANGUAGE SQL IMMUTABLE;
Is there an elegant way to cast a JSONB string array into SQL text array? Why we need this ugly numeric and boolean casts if the internal representation says its datatypes?
EDITED after good #IgorRomanchenko discussion:
PHILOSOPHIC NOTES
My preocupation is
Why PostreSQL 9.5 is losing JSONB internal datatype information and internal (number and boolean) representations?
In nowadays the runtime engine use CPU to convert JSONB number to text and text to SQL numeric, or boolean to text them text to boolean... Ideal (optimized) is "to by-pass" JSONB internal representations, or only casts "binary to binary" (not parse to text and cast text to binary).
PostgreSQL will be more optimized and friendly when internally using JSONB datatype (jsonb_typeof)!
PS1: the SELECT array_agg(x) FROM jsonb_array_elements_text() example only amplifies and exposes the same problem. In this case we also need a direct jsonb_array_to_array(x,type) function to by-pass array bynary representation.
PS2: CREATE CAST is not a solution to this problem, because destroys the original JSONB representations.
... How to do it?
Hum... Perhaps creating alternative casting types like ::numericByPass to "bypass JSONB binary number to SQL numeric" and ::booleanByPass "bypass JSONB boolean to SQL boolean", so, preserving internal representation, not parsing it as a text value. Of course, if runtime check see string instead expected datatype, it triggers an error.
Looking at the example we see that the context obligates to use number and boolean, the user not need to say it to the compiler.
The SQL parser can use this implicit "context obligation" to do by it self, in compile time, the cast to ::numericByPass and ::booleanByPass, preserving JSONB internal representations. If the user is really afraid about string inputs, only in this case the user will add explicit ::numeric and ::boolean castings.
You need runtime casts and checks because an input JSON does not define any structure. You can not check that this input JSON has field "list" with text array in it or a field "lim" with an integer.
Take this example JSON:
{
"list":42,
"lim": [3,5],
"osort": "maybe"
}
Current function will throw type cast exceptions because it has runtime casts and checks. What will this function do without any runtime checks?
BTW If you really want postgres to do this for you - you can enable implicit cast from varchar to int and boolean as in this post: Postgresql. CREATE CAST 'character varying' to 'integer'
It is not the best idea, but it will work.
You can create different operators similar to ->> that produce different output types. For example, -># could give you a number and ->| a boolean.
CREATE FUNCTION json_numeric(
j json
, e text
) RETURNS NUMERIC IMMUTABLE STRICT LANGUAGE SQL AS $$
SELECT (j->>e)::numeric
$$;
CREATE OPERATOR -># ( PROCEDURE = json_numeric, LEFTARG = json, RIGHTARG = text );
SELECT ('{"a": 42, "b": "1"}')::json -># 'a';
?column?
----------
42
(1 row)
SELECT ('{"a": 42, "b": "1"}')::json -># 'b';
?column?
----------
1
(1 row)
If you used plpgsql you could throw an error if json_typeof() wasn't correct, but note that will be significantly slower than the SQL version (though in most usage that won't matter).

How do I pass in a table parameter to this function?

I have a function organized like so:
create function everything(waypoints waypoint)
returns table(node int, xy text array) as $$
BEGIN
create view results as ...
return query (select * from results);
END;
$$ LANGUAGE plpgsql;
And I have a table that has arguments organized the way the waypoint data type is structured. This table is not explicitly of type waypoint itself.
The function gets created as it should, however, I am unable to call it by passing in my table like so:
select everything(waypoints);
Or select everything(select * from temp);
But it says syntax error at or near select for the latter and column waypoints does not exist for the former.
How do I proceed?
Everything tested in Postgres 9.4.
Postgres has some weak spots in the syntax for handling ROW types. You cannot cast from a table (alias) directly:
SELECT w::waypoint FROM waypoints w;
ERROR: cannot cast type waypoints to waypoint
The solution is only one step away: decompose the row in a subquery, then the cast works. This way, column values are decomposed and wrapped into the new type directly, without casting to text and back. No need to list all columns individually and you don't need to create a custom cast, either:
SELECT (w.*)::waypoint FROM (SELECT * FROM waypoints) w;
Or shorter:
SELECT w.*::waypoint FROM (TABLE waypoints) w;
Or shorter, yet:
SELECT w::waypoint FROM (TABLE waypoints) w;
Is there a shortcut for SELECT * FROM?
SQL Fiddle
That's shorter and faster, in a quick test with 30k rows and simple types 10x faster than casting to text and back. If you have (big) jsonb columns or any complex type (expensive conversion to/from text), the difference will be much bigger, yet.
More importantly, you don't need another custom composite (ROW) type. Every table already has its row defined as type automatically. Just use the existing type waypoints instead of waypoint (if at all possible). Then all you need is:
SELECT w FROM waypoints w;
Or, for your example:
SELECT everything(t) FROM temp t; -- using type waypoints
SELECT everything(t::waypoint) FROM (TABLE temp) t; -- using type waypoint
Asides:
A table does not have "arguments" but columns.
You are not passing a table parameter to this function, but rather a row value. That's how you pass a table by name:
Table name as a PostgreSQL function parameter
You can't "pass a whole table" as parameter directly in Postgres, there are not table variables. You would use a cursor or a temp table for that.
Function
Your function has an invalid type declaration and is needlessly complex. I seriously doubt you want to create a view:
CREATE FUNCTION everything(_wp waypoint) -- or use type waypoints
RETURNS TABLE(node int, xy text[]) AS
$func$
BEGIN
RETURN QUERY
SELECT ...
END
$func$ LANGUAGE plpgsql;
text array is not valid syntax, using text[] instead to declare an array of text.
Rather not use the table / type name waypoints as function parameter name, that opens you up to confusing errors.
Or just use a simple SQL function if your case is as simple as demonstrated:
CREATE FUNCTION everything(_wp waypoint) -- or use type waypoints
RETURNS TABLE(node int, xy text[]) AS
$func$
SELECT ...
$func$ LANGUAGE sql;
Don't quote the language name. It's an identifier.
If all types in waypoint and temp are convertible to text, you can serialize to and deserialize from text:
SELECT everything(temp::text::waypoint)
FROM temp
However, an explicit construction would be cleaner:
SELECT everything((col1, col2, col3, ...)::waypoint)
FROM temp
or creating a CAST:
CREATE FUNCTION temp_to_waypoint (temp)
RETURNS waypoint
AS
$$
SELECT (col1, col2, col3, ...)::waypoint
$$
LANGUAGE 'sql';
CREATE CAST (temp AS waypoint) WITH FUNCTION temp_to_waypoint (test) AS IMPLICIT;
SELECT everything(temp)
FROM temp;