PostgreSQL nested UDTs ans CAST syntax - postgresql

I have few UD types:
CREATE TYPE SOME_TYPE AS (
aaa INT,
bbb TEXT
);
CREATE TYPE SOME_ANOTHER_TYPE AS (
ccc int,
ddd SOME_TYPE
);
CREATE TYPE SOME_ANOTHER_DAMNIT_TYPE AS (
eee int,
fff SOME_ANOTHER_TYPE
);
Okay, i can make first from SQL:
select '(123,blablabla)'::SOME_TYPE;
And i can make second:
select '(456, "(123,blablabla)")'::SOME_ANOTHER_TYPE;
But if i try to make third...
select '(789,"(456, "(123,blablabla)")")'::SOME_ANOTHER_DAMNIT_TYPE;
... pl throw error:
[22P02] ERROR: malformed record literal: "(456, "(123 Подробности:
Unexpected end of input.
Question: how to make third type without plpgsql? I need it for calling from jdbc, to transfer object. I know, i can 'flatten' this object, but i want transfer it 'as is'.

The problem is that you have nested the quotes without escaping the inner quotes. You could that like so:
select '(789,"(456, ""(123,blablabla)"")")'::SOME_ANOTHER_DAMNIT_TYPE;
i.e. by doubling the ".
For your case, I would suggest to avoid representing your record values as text only to cast them as your type later on. You can specify the record values directly, without going through text like so:
select (789,(456, (123,'blablabla')))::SOME_ANOTHER_DAMNIT_TYPE;

Related

What is PostgreSQL syntax for complex array literals? (e.g. circle[])

The PostgreSQL docs state that, though some literals have special syntax, generic syntax for literal values looks like type 'data', 'data'::type, or CAST('data' AS type). For instance, one could write the integer 16 as 16 or as '16'::int. Dollar-quoting is also allowed, so $$16$$:int works as well.
For some types, such as circle, the generic syntax is (as far as I can tell) the only way to write a literal. Four syntaxes are listed for circle: <(x, y), r>, ((x, y), r), (x, y), r, and x, y, r; however, none of these seem to work plain:
y=> create table t (c circle);
CREATE TABLE
y=> insert into t (c) values ( <(1,2),3> );
ERROR: syntax error at or near "<"
LINE 1: insert into t (c) values ( <(1,2),3> );
^
y=> insert into t (c) values ( ((1,2),3) );
ERROR: column "c" is of type circle but expression is of type record
LINE 1: insert into t (c) values ( ((1,2),3) );
^
HINT: You will need to rewrite or cast the expression.
y=> insert into t (c) values ( (1,2),3 );
ERROR: INSERT has more expressions than target columns
LINE 1: insert into t (c) values ( (1,2),3 );
^
y=> insert into t (c) values ( 1,2,3 );
ERROR: INSERT has more expressions than target columns
LINE 1: insert into t (c) values ( 1,2,3 );
^
On the contrary, using the generic syntax works fine:
y=> insert into t (c) values ( '1,2,3' );
INSERT 0 1
y=> select c from t;
c
-----------
<(1,2),3>
Also documented is the array syntax, which is comma-delimited and curly-enclosed. So the range 1-5 may be written, for instance, as '{1, 2, 3, 4, 5}'::int[].
Unfortunately, the array syntax does not seem to support values specified with the generic syntax: $${ 1 }$$::int[] is accepted, but $${ '1'::int }::int[] is rejected, as is $${ '1' }::int[].
Because array literal syntax does not accept generic-form syntax for elements, and because generic syntax seems to be the only way to write a circle literal, it appears that it is impossible to write a circle literal in PostgreSQL.
y=> select $${ }$$::circle[];
circle
--------
{}
(1 row)
y=> select $${ '<(1, 2), 3>'::circle }$$::circle[];
ERROR: invalid input syntax for type circle: "'<(1"
LINE 1: select $${ '<(1, 2), 3>'::circle }$$::circle[];
Is this indeed the case?
Note: yes, it's crucial that I want to write a circle[] literal. The reason is that my use-case is for specifying values in prepared statements in a PostgreSQL client, and, as the PostgreSQL protocol documentation says (emphasis mine):
Parameter data types can be specified by OID; if not given, the parser attempts to infer the data types in the same way as it would do for untyped literal string constants.
This means that, for instance, ARRAY[ '<(1,2),3>'::circle ] is not a valid solution, as it does not use literal syntax.
Turns out the solution is simple: escape the commas!
y=> select '{ <(1\,2)\,3>, <(4\,5)\,6> }'::circle[];
circle
---------------------------
{"<(1,2),3>","<(4,5),6>"}
(1 row)
Also an option is to double-quote the elements:
y=> select '{ "<(1,2),3>", "<(4,5),6>" }'::circle[];
circle
---------------------------
{"<(1,2),3>","<(4,5),6>"}
(1 row)
Credit to sehrope on Github for telling me this. Quote from the relevant part of the PostgreSQL docs:
[...] when writing an array value you can use double quotes around any individual array element. You must do so if the element value would otherwise confuse the array-value parser. For example, elements containing curly braces, commas [...], double quotes, backslashes, [etc,] must be double-quoted. [...] Alternatively, you can avoid quotes and use backslash-escaping to protect all data characters that would otherwise be taken as array syntax.

Conversion failure varchar to int on a column cast as int

I've created a view called vReceivedEmail which includes a varchar column called RowPrimaryKeyValue. This column usually stores primary key id values, such as 567781, but every now and then has the descriptive text 'Ad hoc message'.
I've set the view up so that it only shows records that would hold the primary key values and then CAST the column as int.
SELECT CAST(Email.RowPrimaryKeyValue as int) as DetailID
FROM MessageStore.dbo.Email Email
WHERE ISNUMERIC(Email.RowPrimaryKeyValue) = 1
When I test the view, I only get records with the primary key values I expected, and when I look at the column listing for the view in the object explorer, the column is saved at the int data type.
I've then tried to apply the following WHERE clause in a separate query, referencing my saved view:
SELECT PotAcc.DetailID
FROM PotentialAccounts PotAcc
WHERE PotAcc.DetailID NOT IN (
SELECT RecEmail.DetailID
FROM vReceivedEmail RecEmail
)
...but I'm returning the following error:
Conversion failed when converting the varchar value 'Ad hoc message'
to data type int.
'Ad hoc message' is data from the column I filtered and converted to int, but I don't know how it's managing to trip up on this since the view explcitly filters this out and converts the column to int.
Since you are on SQL Server 2012, how about using try_convert(int,RowPrimaryKeyValue) is not null instead of isnumeric()?
Your view would look like so:
select try_convert(int, RowPrimaryKeyValue) as DetailID
from MessageStore.dbo.Email Email
where try_convert(int, RowPrimaryKeyValue) is not null
In SQL Server 2012 and up: each of these will return null when the conversion fails instead of an error.
try_convert(datatype,val)
try_cast(val as datatype)
try_parse(val as datatype [using culture])
Why doesn’t isnumeric() work correctly? (SQL Spackle)
Isnumeric() can be deceiving... however I'm betting this is due to SQL Server optimization. Though your view works, that doesn't mean the outer query guarantees that only INT is returned. Instead of using a view... use a CTE and try it. And since you are on 2012, this can all be avoided how SqlZim explained above.
WITH CTE AS(
SELECT CAST(Email.RowPrimaryKeyValue as int) as DetailID
FROM MessageStore.dbo.Email Email
WHERE ISNUMERIC(Email.RowPrimaryKeyValue) = 1)
SELECT PotAcc.DetailID
FROM PotentialAccounts PotAcc
WHERE PotAcc.DetailID NOT IN (
SELECT RecEmail.DetailID
FROM CTE RecEmail
)

Use function result as columns in query with PostgreSQL

I'm using PostgreSQL 9.1 and let's say I have a type in PostgreSQL like:
CREATE TYPE sticker AS (
customer_id integer,
customer_machine_id integer,
);
and I have a plpgsql named get_sticker that returns the type sticker...
I can do this fine:
select get_sticker(a_value), * from foo_bar;
But, this returns the result in a tuple (which totally makes sense). But, how can I convert (basically unpack) to columns?
It seems like it would be something like the following, but it fails.
select get_sticker(a_value).*, * from foo_bar; -- <<<< FAIL
Error message:
ERROR: syntax error at or near "."
SQL state: 42601
You need an extra set of parentheses:
select (get_sticker(a_value)).*, * from foo_bar;

SQL invalid conversion return null instead of throwing error

I have a table with a varchar column, and I want to find values that match a certain number. So lets say that column contains the following entries (except with millions of rows in real life):
123456789012
2345678
3456
23 45
713?2
00123456789012
So I decide I want all the rows which are numerically 123456789012 write a statement that looks something like this:
SELECT * FROM MyTable WHERE CAST(MyColumn as bigint) = 123456789012
It should return the first and last row, but instead the whole query blows up because it can't convert the "23 45" and "713?2" to bigint.
Is there another way to do the conversion that will return NULL for values that can't convert?
SQL Server does NOT guarantee boolean operator short-circuit, see On SQL Server boolean operator short-circuit. So all solution using ISNUMERIC(...) AND CAST(...) are fundamentally flawed (they may work, but hey can arbitrarily fail later dependiong on the generated plan). A better solution is using CASE, as Thomas suggests: CASE ISNUMERIC(...) WHEN 1 THEN CAST(...) ELSE NULL END. But, as gbn pointed out, ISNUMERIC is notoriously finicky in identifying what 'numeric' means and many cases where one would expect it to return 0 it returns 1. So mixing the CASE with the LIKE:
CASE WHEN MyRow NOT LIKE '%[^0-9]%' THEN CAST(MyRow as bigint) ELSE NULL END
But the real problem is that if you have millions of rows and you have to search them like this, you'll always end up scanning end-to-end since the expression is not SARG-able (no matter how we rewrite it). The real issue here is data purity, and should be addressed at the appropriate level, where the data is populated. Another thing to consider is if is possible to create a persisted computed column with this expression and create a filtered index on it which eliminates NULL (ie. non-numeric). That would speed up things a little.
If you are using SQL Server 2012 you can use the 2 new methods:
TRY_CAST()
TRY_CONVERT()
Both methods are equivalent. They return a value cast to the specified data type if the cast succeeds; otherwise, returns null. The only difference is that CONVERT is SQL Server specific, CAST is ANSI. using CAST will make your code more portable (although not sure if any other database provider implements TRY_CAST)
ISNUMERIC will accept empty string and values like 1.23 or 5E-04 so could be unreliable.
And you don't know what order things will be evaluated in so it could still fail (SQL is declarative, not procedural, so the WHERE clause probably won't be evaluated left to right)
So:
you want to accept value that consist only of the characters 0-9
you need to materialise the "number" filter so it's applied before CAST
Something like:
SELECT
*
FROM
(
SELECT TOP 2000000000 *
FROM MyTable
WHERE MyColumn NOT LIKE '%[^0-9]%' --double negative rejects anything except 0-9
ORDER BY MyColumn
) foo
WHERE
CAST(MyColumn as bigint) = 123456789012 --applied after number check
Edit: quick example that fails.
CREATE TABLE #foo (bigintstring varchar(100))
INSERT #foo (bigintstring )VALUES ('1.23')
INSERT #foo (bigintstring )VALUES ('1 23')
INSERT #foo (bigintstring )VALUES ('123')
SELECT * FROM #foo
WHERE
ISNUMERIC(bigintstring) = 1
AND
CAST(bigintstring AS bigint) = 123
SELECT *
FROM MyTable
WHERE ISNUMERIC(MyRow) = 1
AND CAST(MyRow as float) = 123456789012
The ISNUMERIC() function should give you what you need.
SELECT * FROM MyTable
WHERE ISNUMERIC(MyRow) = 1
AND CAST(MyRow as bigint) = 123456789012
And to add a case statement like Thomas suggested:
SELECT * FROM MyTable
WHERE CASE(ISNUMERIC(MyRow)
WHEN 1 THEN CAST(MyRow as bigint)
ELSE NULL
END = 123456789012
http://msdn.microsoft.com/en-us/library/ms186272.aspx
SELECT *
FROM MyTable
WHERE (ISNUMERIC(MyColumn) = 1) AND (CAST(MyColumn as bigint) = 123456789012)
Additionally you can use a CASE statement in order to get null values.
SELECT
CASE
WHEN (ISNUMERIC(MyColumn) = 1) THEN CAST(MyColumn as bigint)
ELSE NULL
END AS 'MyColumnAsBigInt'
FROM tableName
If you require additional filtering, for numerics which are not valid to be cast to bigint, you can use the following instead of ISNUMERIC:
PATINDEX('%[^0-9]%',MyColumn)) = 0
If you need decimal values instead of integers, cast to float instead and change the regex to '%[^0-9.]%'

Postgres query error

I have a query in postgres
insert into c_d (select * from cd where ak = '22019763');
And I get the following error
ERROR: column "region" is of type integer but expression is of type character varying
HINT: You will need to rewrite or cast the expression.
An INSERT INTO table1 SELECT * FROM table2 depends entirely on order of the columns, which is part of the table definition. It will line each column of table1 up with the column of table2 with the same order value, regardless of names.
The problem you have here is whatever column from cd with the same order value as c_d of the table "region" has an incompatible type, and an implicit typecast is not available to clear the confusion.
INSERT INTO SELECT * statements are stylistically bad form unless the two tables are defined, and will forever be defined, exactly the same way. All it takes is for a single extra column to get added to cd, and you'll start getting errors about extraneous extra columns.
If it is at all possible, what I would suggest is explicitly calling out the columns within the SELECT statement. You can call a function to change type within each of the column references (or you could define a new type cast to do this implicitly -- see CREATE CAST), and you can use AS to set the column label to match that of your target column.
If you can't do this for some reason, indicate that in your question.
Check out the PostgreSQL insert documentation. The syntax is:
INSERT INTO table [ ( column [, ...] ) ]
{ DEFAULT VALUES | VALUES ( { expression | DEFAULT } [, ...] ) | query }
which here would look something like:
INSERT INTO c_d (column1, column2...) select * from cd where ak = '22019763'
This is the syntax you want to use when inserting values from one table to another where the column types and order are not exactly the same.