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?)

LondonRob
  • 73,083
  • 37
  • 144
  • 201
  • 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 https://dba.stackexchange.com/questions/194975/how-to-avoid-implicit-type-casts-in-postgresql – doctore Sep 17 '20 at 17:25
  • Have you tried dropping the assignment cast from `float8` to `integer`? – Laurenz Albe Sep 17 '20 at 18:09
  • << spends an hour reading dense [type-conversion documentation](https://www.postgresql.org/docs/12/typeconv-oper.html). Decides to stop thinking about ths issue forever >> – LondonRob Sep 21 '20 at 13:19
  • Just when I am about to commit an answer to make you think about it some more. :p – Erwin Brandstetter Sep 21 '20 at 13:26
  • @ErwinBrandstetter lol. No, I'm actually really interested (and I'm still reading the docs despite my earlier comment!) – LondonRob Sep 21 '20 at 13:40

1 Answers1

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:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • 1
    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 Sep 21 '20 at 14:38
  • 1
    @LondonRob: I added another possible solution. – Erwin Brandstetter Sep 21 '20 at 16:41
  • Nice. This is the kind of serious pg_wizardry I enjoy. – LondonRob Sep 22 '20 at 14:48