2

As far as I was able to find out, it is currently not possible in PostgreSQL v9.5 (and above?) to obtain implicit casting with custom range types. To illustrate, when I define the following demo setup:

drop schema if exists tac cascade;
create schema tac;

create domain tac.usascii_small_letters
  as text
  check ( value ~ '^[a-z]+$' );

create domain tac.ucid as integer
  check ( value between x'0'::integer and x'10ffff'::integer );

create function tac.ucid_rng_diff( cid_1 tac.ucid, date_2 tac.ucid )
  returns double precision
  language sql
  immutable
  as $$
    select cast( cid_1 - date_2 as double precision );
  $$;

-- A ----------------------------------------------------------------
create type tac.ucid_rng as range (
  subtype       = tac.ucid,
  subtype_diff  = tac.ucid_rng_diff
  );
-- ----------------------------------------------------------------

-- -- B ----------------------------------------------------------------
-- set role dba; -- !!!!!!!!!!!!!!!!!!!!
-- create type tac.ucid_rng;

-- create function tac.ucid_rng_canonical( x tac.ucid_rng )
--   returns tac.ucid_rng
--   language plpgsql
--   as $$
--     begin
--         if not lower_inc(x) then
--             x := tac.ucid_rng(lower(x) + 1, upper(x), '[]');
--         end if;
--         if not upper_inc(x) then
--             x := tac.ucid_rng(lower(x), upper(x) - 1, '[]');
--         end if;
--         return x;
--     end;
--   $$;

-- create type tac.ucid_rng as range (
--   subtype       = tac.ucid
--   , subtype_diff  = tac.ucid_rng_diff
--   , canonical     = tac.ucid_rng_canonical
--   );

-- reset role; -- !!!!!!!!!!!!!!!!!!!!
-- -- ----------------------------------------------------------------

create table tac.words (
  word                    tac.usascii_small_letters,
  cid                     tac.ucid,
  cid_range               tac.ucid_rng
  );

insert into tac.words values
  ( 'foo', 1, '[11,21]' ),
  ( 'bar', 2, '[12,22]' ),
  ( 'zip', 3, '[13,23]' ),
  ( 'dat', 4, '[14,24]' ),
  ( 'baz', 5, '[15,25]' );

select * from tac.words where word between 'a' and 'c';
select * from tac.words where word between 'a'::tac.usascii_small_letters and 'c';
select * from tac.words where cid between 3 and 5;
select * from tac.words where cid_range @> '[17,23]';
select * from tac.words where cid_range @> '[17,23]'::tac.ucid_rng;
select * from tac.words where cid_range @> 23::tac.ucid;
select * from tac.words where cid_range @> 23;

and run it as is with block A but without block B, I get

ERROR:  operator does not exist: integer <@ tac.ucid_rng
LINE 1: select * from tac.words where cid_range @> 23;
HINT:  No operator matches the given name and argument type(s).
You might need to add explicit type casts.

The tests show that a lot of things using custom domains / types do work with implicit casting. Indeed, I can do a kind-of range check against my custom string data type, and even cid_range @> '[17,23]' works. It is only cid_range @> 23 that mysteriously fails.

Now I tried to implement a suitable canonicalization function for my tac.cid_rng type; however, as remarked in https://stackoverflow.com/a/29939205, that's not gonna fly:

NOTICE:  argument type tac.ucid_rng is only a shell
NOTICE:  return type tac.ucid_rng is only a shell
ERROR:  PL/pgSQL functions cannot return type tac.ucid_rng

So it would seem that it's still impossible in PostgreSQL 9.5 to have a custom range type with implicit casting in point-inclusion queries, although the type itself is defined quite transparently over a set of contiguous integers and similar queries work fine without any extra coding.

I'd like to know whether I missed something here, and what recommended ways there are to deal with the situation. I think I could just live with the explicit cast, or else write a function that does it for me.

The thing I don't get is why cid_range @> '[17,23]' works but cid_range @> 23 fails—after all, a cast from generic / suggestive literal to actual data type has to be performed in both cases. It would appear from the tests that implicit casting from integer literal into the tac.ucid domain work as well, so it's hard to see what exactly is missing that necessitates a piece of C boiler plate code that does nothing but a generic check and integer increment / decrement.

1

I think this is a bug and you should report it. I think it can be demonstrated a lot easier.

SELECT '[1,5]'::int4range @> 3;

CREATE DOMAIN zdomain AS int;
CREATE TYPE myrange_int     AS RANGE ( subtype = int );
CREATE TYPE myrange_zdomain AS RANGE ( subtype = zdomain );

-- WORKS
SELECT '[1,5]'::myrange_int     @> 3;
SELECT '[1,5]'::myrange_zdomain @> 3::zdomain;

-- DOES NOT WORK
SELECT '[1,5]'::myrange_zdomain @> 3;
ERROR:  operator does not exist: myrange_zdomain @> integer
LINE 1: SELECT '[1,5]'::myrange_zdomain @> 3;
                                          ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.

I think what you want is totally reasonable. I don't think it's going to be a high priority item. My assumption is that coercion that is triggered is not looking to see if the range is over a domain, and it should.

Feel free to update your question with my example, I think it'll be a lot easier for people to follow

  • 1
    Nice to find out... – joanolo Jun 16 '17 at 21:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.