6

I have a table like:

CREATE TABLE foo(bar int)

I have a script which inserts values into that table:

INSERT INTO foo(bar)
VALUES (1), (2), (3.2)

The float value is silently rounded to fit the data type:

> SELECT * FROM foo;
 bar
-----
   1
   2
   3
(3 rows)

Is there anything built-in to Postgres which can prevent this from happening and, instead, raise an error? (Or even a warning?)

5
  • Neither a check like check (bar % 1 = 0) in the table definition nor a before insert trigger will allow you obtain the "source data", because in both cases 3.2 has been converted to 3 previously (due to type definition and implicit conversion). So, not sure if is possible to do what you want. The following link will provide more information about it dba.stackexchange.com/questions/194975/… Commented Sep 17, 2020 at 17:25
  • Have you tried dropping the assignment cast from float8 to integer? Commented Sep 17, 2020 at 18:09
  • << spends an hour reading dense type-conversion documentation. Decides to stop thinking about ths issue forever >> Commented Sep 21, 2020 at 13:19
  • Just when I am about to commit an answer to make you think about it some more. :p Commented Sep 21, 2020 at 13:26
  • @ErwinBrandstetter lol. No, I'm actually really interested (and I'm still reading the docs despite my earlier comment!) Commented Sep 21, 2020 at 13:40

1 Answer 1

3

The numeric constant 3.2 initially resolves to data type numeric (not float). Details in the manual here.

The assignment to an integer columns works silently because there is an "assignment" cast registered for numeric --> integer in standard Postgres.

To get the desired behavior, you would have to hack a warning into the function that implements the cast in the Postgres source code (and recompile). A very high price to pay. (What about version updates?)

Or you remove or change the registered cast. You could replace the cast with a version based on your own function - and raise a WARNING in there. But that's expensive, and may spam a lot of warnings.

You don't want to remove that cast completely. Lots and lots of calculations use it.

Workaround?

You could use this simplistic workaround: disable the cast for the transaction only:

BEGIN;

UPDATE pg_cast
SET    castcontext = 'e'               -- make the cast "explicit"
WHERE  castsource = 'numeric'::regtype
AND    casttarget = 'integer'::regtype;

INSERT INTO foo(bar)
VALUES (1), (2), (3.2);

UPDATE pg_cast
SET    castcontext = 'a'               -- revert back to "assignment"!
WHERE  castsource = 'numeric'::regtype
AND    casttarget = 'integer'::regtype;

COMMIT;

Now, an exception is raised in case of actual numeric input. But you need superuser privileges to do that. You might encapsulate it in a SECURITY DEFINER function. Related:

And you don't want to lock the system catalog pg_cast for long while concurrent operations can happen. So I'd rather not do that with concurrency around.

Solution?

You could move input values to a CTE and test in a WHERE clause to skip inserts (silently) if they aren't all valid integer values:

WITH input(i) AS (
   VALUES (1), (2), (3.2)  -- one numeric makes all fall back to numeric
   )
INSERT INTO foo (bar)
SELECT i
FROM   input
WHERE  (SELECT pg_typeof(i) FROM input LIMIT 1) = 'integer'::regtype;

db<>fiddle here

You could then check the command tag whether anything was inserted.

Or wrap it all in a plpgsql function, check whether anything was actually inserted, and RAISE whatever you need if not. Related examples:

Sign up to request clarification or add additional context in comments.

3 Comments

This is really interesting stuff. It's important to know about this kind of blood-and-guts detail of how Postgres works. We're still developing a green-field project so we could potentially get away with this solution. But I suspect most people will read this and conclude that trapping the input in scripting is the best way forward.
@LondonRob: I added another possible solution.
Nice. This is the kind of serious pg_wizardry I enjoy.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.