Background:

Domains lay the groundwork for inherited basetypes
or subtypes.  By defining a domain and overriding 
operators and possibly creating an operator class, then a domain
can be created which inherits the storage method
and all of the functions of a basetype.  The domain
constraint enables a psuedo input function for
the subtype. 

Domains enable people to create basetype subtypes using SQL
and procedural languages only.  Current belief is that
this "doesn't work."  However, all of this has worked
since domains were implemented with three exceptions.

1. PRIMARY KEY uses a type's base type opclass indiscriminantly.
   Workaround: for this is to create an opclass for
        the subtype and create a UNIQUE index on that column.

2. Operators which take two different types are not found.
        The key example for this is a LIKE or ~~ type for a
        subtype of text which has as its operands (basetype, parenttype).
        Workaround: use the function instead of the operator.

3. ORDER BY uses the parent type's sort operators.
        Workaround: Use ORDER BY ... USING 'operator'

Proposal:

This proposal only addresses issue #2.  I will look into the
other two as time permits.  But there are sensible workarounds
to #1 and #3 in the meanwhile.

This patch passes regression as well as my domain tests.

Attached is a patch to parse_oper.c which essentially does the
following.  The major change is in binary_oper_exact().
Instead of checking only one level of the basetype it checks
all possible combinations of type and parent types for
an exact match (only).  This favors first the passed in type
and then the basetype for exact matches.  The second part of 
this change is to lightly loosen the assumption that any operator 
has same type operands.  If an exact match is not made, the UNKNOWNOID
setting for the original operator is re-instated so that
the function selection mechanism can give it a try.

Tests:

Also attached are two files to create a domain/subtype and
to test it.
 
Issues/Questions:

1) Code review please.
2) Are there any test cases that were not covered by regression
   and my tests?
3) Should I add my test case to the regression tests?
4) Eventually this feature should be added to the docs.  Should
   I wait until all of the pieces are in place or go ahead and
   document the feature as it stands?

elein
--------------------------------------------------------------
[EMAIL PROTECTED]        Varlena, LLC        www.varlena.com
          PostgreSQL Consulting, Support & Training   
PostgreSQL General Bits   http://www.varlena.com/GeneralBits/
--------------------------------------------------------------
I have always depended on the [QA] of strangers.

--
-- create type email under text;
--
-- Elein Mustain, [EMAIL PROTECTED], http://www.varlena.com
-- Published 19-Feb-2006 at http://www.varlena.com/GeneralBits/128.php
-- License: BSD, Share and enjoy
--

drop domain email cascade;

--
-- Check constraint isemail for email base type
-- Using "pretty good" but not strict rfc compliant regex
-- cf: http://aspn.activestate.com/ASPN/Cookbook/Rx/Recipe/59886
--
CREATE OR REPLACE FUNCTION isemail(text)
RETURNS BOOLEAN
AS $$
   if ( $_[0] =~ m/
       ^                  # Start.
        (                 # Start of $1
            [A-Z0-9]+     # Must start with some A-Z or 0-9s.
            [._]?         # May have a . or an _ after that
        )                 # End of $1
        {1,}              # One or more of $1.
        [A-Z0-9]+         # More alphanumerics
        \@                # Literal @ sign
        (                 # Start of $2
            (             # Start of $3
                [A-Z0-9]+ # More alphanumerics
                [-]?      # At most one - sign.
            )             # End of $3
            {1,}          # One or more of $3
            [A-Z0-9]+\.   # More alphanumerics
        )                 # End of $2
        {1,}              # One or more of $2.
        [A-Z]{2,4}        # 2-4 letters
        $                 # End.
    /ix ) {
      return TRUE;
   }
   else {
      return FALSE;
   }
$$ LANGUAGE 'plperl' STRICT IMMUTABLE;

--
-- create type email under text
--
CREATE DOMAIN email AS text CHECK ( isemail( VALUE) );

--
-- Comparison and Equality Functions
--
-- cmp: 0 if equal, 1 if greater than, -1 if less than
CREATE OR REPLACE FUNCTION email_cmp(email,email)
RETURNS INTEGER AS
$$
        SELECT CASE WHEN lower($1) = lower($2) THEN 0
                    WHEN lower($1) > lower($2) THEN 1
        ELSE -1
        END;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

-- Equals: lower(text) = lower(text)
CREATE OR REPLACE FUNCTION email_eq (email, email) RETURNS boolean AS
$$
        SELECT CASE WHEN email_cmp($1, $2) = 0 
        THEN TRUE ELSE FALSE END;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

-- Greater than: lower(text) > lower(text)
CREATE OR REPLACE FUNCTION email_gt (email, email) RETURNS boolean AS
$$
        SELECT CASE WHEN email_cmp($1, $2) > 0 
        THEN TRUE ELSE FALSE END;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

-- Less than: lower(text) < lower(text)
CREATE OR REPLACE FUNCTION email_lt (email, email) RETURNS boolean AS
$$
        SELECT CASE WHEN email_cmp($1, $2) < 0 
        THEN TRUE ELSE FALSE END;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

--Greater than or Equals
CREATE OR REPLACE FUNCTION email_gte (email, email) RETURNS boolean AS
$$
        SELECT CASE WHEN email_cmp($1,$2) = 0 OR email_cmp($1,$2) = 1
        THEN TRUE ELSE FALSE end;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

--Less than or Equals
CREATE OR REPLACE FUNCTION email_lte (email, email) RETURNS boolean AS
$$
        SELECT CASE WHEN email_cmp($1,$2) = 0 OR email_cmp($1,$2) = -1
        THEN TRUE ELSE FALSE END;
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

--Like
CREATE OR REPLACE FUNCTION email_like(email, text) RETURNS boolean AS
$$
        SELECT lower($1) LIKE lower($2);
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;

--
-- Operators
--
CREATE OPERATOR =  ( PROCEDURE = email_eq, LEFTARG = email, RIGHTARG = email);
CREATE OPERATOR >  ( PROCEDURE = email_gt, LEFTARG = email, RIGHTARG = email);
CREATE OPERATOR >= ( PROCEDURE = email_gte, LEFTARG = email, RIGHTARG = email);
CREATE OPERATOR <  ( PROCEDURE = email_lt, LEFTARG = email, RIGHTARG = email);
CREATE OPERATOR <= ( PROCEDURE = email_lte, LEFTARG = email, RIGHTARG = email);
CREATE OPERATOR ~~  ( PROCEDURE = email_like, LEFTARG = email, RIGHTARG = text);

--
-- Operator Class
--
CREATE OPERATOR CLASS email_ops
DEFAULT FOR TYPE email USING BTREE AS
        OPERATOR 1 <,
        OPERATOR 2 <=, 
        OPERATOR 3 =,
        OPERATOR 4 >=,
        OPERATOR 5 >,
        FUNCTION 1 email_cmp(email,email)
;

--
-- Accessor functions
--
-- login: before the @ sign
CREATE OR REPLACE FUNCTION email_login (email) RETURNS text AS
$$
        return (split(/\@/, $_[0]))[0];
$$ LANGUAGE 'plperl' IMMUTABLE STRICT;

-- Full Domain: after the @ sign
CREATE OR REPLACE FUNCTION email_fdomain (email) RETURNS text AS
$$
        return  (split /\@/, $_[0])[-1];
$$ LANGUAGE 'plperl' IMMUTABLE STRICT;

-- Top Level Domain:: after the last dot
CREATE OR REPLACE FUNCTION email_domain_type (email) RETURNS text AS
$$
        return (split(   # Split the output of split below
        /\./,  # Split the following on "."
        (
            split(
                /\@/,  # Split whole input on "@"
                $_[0]
            )
        )[-1]  # last part of inner split
    ))[-1];  # last part of outer split
$$ LANGUAGE 'plperl' IMMUTABLE STRICT;

-- Reverse Domain
CREATE OR REPLACE FUNCTION email_reverse_domain(email) RETURNS text AS
$$
   return join(".", reverse split(   # Split the output of split below
        /\./,  # Split the following on "."
        (
            split(
                /\@/,  # Split whole input on "@"
                $_[0]
            )
        )[-1]  # last part of inner split
    ));
$$ LANGUAGE 'plperl' IMMUTABLE STRICT;

-- Nonsense function to ensure func select will choose this one
CREATE OR REPLACE FUNCTION email_n(email, integer) returns text as
$$
        select $1::text || '-' || $2::numeric
$$ LANGUAGE 'SQL' IMMUTABLE STRICT;
create operator * ( PROCEDURE= email_n, LEFTARG=email, RIGHTARG=integer);
--
-- Test create type email under text;
--
-- Elein Mustain, [EMAIL PROTECTED], http://www.varlena.com
-- Published 19-Feb-2006 at http://www.varlena.com/GeneralBits/128.php
-- License: BSD, Share and enjoy
--

drop table aliases;
Create table aliases (
        email   email UNIQUE PRIMARY KEY, -- broken: use unique index with 
btree (email email_ops)
        lname   text
);
create unique index aliases_email on aliases using btree (email email_ops);

\echo [Expect check_email ERROR]
insert into aliases values ('[EMAIL PROTECTED]', 'Bad');
\echo
\echo [Expect PK ERROR]
insert into aliases values ('[EMAIL PROTECTED]', 'PK');
insert into aliases values ('[EMAIL PROTECTED]', 'PK');
\echo
\echo [No errors]
insert into aliases values ('[EMAIL PROTECTED]', 'Boz');
insert into aliases values ('[EMAIL PROTECTED]', 'Abe');
insert into aliases values ('[EMAIL PROTECTED]', 'Carolina');
insert into aliases values ('[EMAIL PROTECTED]', 'Donald');
insert into aliases values ('[EMAIL PROTECTED]', 'ET');
insert into aliases values ('[EMAIL PROTECTED]', 'Frodo');
insert into aliases values ('[EMAIL PROTECTED]', 'Sir');

-- 
\echo [Accessor functions]
select email, email_login(email) from aliases limit 2;
select email, email_fdomain(email) from aliases limit 2;
select email, email_domain_type(email) from aliases limit 2;
select email, email_reverse_domain(email) from aliases limit 2;
\echo

\echo [Equality tests]
select '[EMAIL PROTECTED]'::email = '[EMAIL PROTECTED]'::email; --works
select lname, email from aliases where email = '[EMAIL PROTECTED]'; -- works 
select a.email, a2.lname from aliases a JOIN aliases a2 USING (email); --works
select a.email, a2.lname from aliases a JOIN aliases a2 ON 
(UPPER(a.email)::email = a2.email); --works

\echo [Comparison Operators]
\echo [result should be comparison based on lower()] 
\echo [one sided Casting is required for literals.]
select '[EMAIL PROTECTED]'::email < '[EMAIL PROTECTED]'; -- works 
select '[EMAIL PROTECTED]'::email > '[EMAIL PROTECTED]'; -- works 
select email from aliases where email <= '[EMAIL PROTECTED]'; -- works 
select email from aliases where email >= '[EMAIL PROTECTED]'; -- works 


\echo [LIKE  and ~~ with typed right operand]
\echo select column like value: works
select lname, email from aliases where email like 'BOZO%'::text;  --works
select lname, email from aliases where email ~~ 'BOZO%'::text; --works
select lname, email from aliases where email like lname || '%'; --works 

\echo  [direct function all to email_like works without casts]
select lname, email from aliases where email_like(email, 'BOZO%'); --works
select lname, email from aliases where email_like(email, lname || '%'); --works

-- these now work because email is a child of text.
\echo [Patch Corrected Cases]
\echo [LIKE and ~~ with unknown Right operand untyped work with patch]
select '[EMAIL PROTECTED]'::email like 'ABC%'; --now works-- broken: uses text
select lname, email from aliases where email like 'BOZO%'; --now works -- 
broken 
select lname, email from aliases where email ~~ 'BOZO%'; --now works -- broken 


\echo 
\echo [ORDER BY]
\echo [Have to add using clause to tell which sort operator to use]
\echo [ASC/DESC: dont change things]
select * from aliases order by email; --broken 
\echo Using USING < > works correctly
select * from aliases order by email using <;
select * from aliases order by email using >;
\echo

\echo 
\echo MISC Tests
select email ~~ upper(email) from aliases; -- now works uses email ~~
select email || email from aliases;  -- works uses text ||
select email_n(email,2) from aliases; -- works
select email * 2 from aliases; -- works: uses email_n
Index: parse_oper.c
===================================================================
RCS file: /projects/cvsroot/pgsql/src/backend/parser/parse_oper.c,v
retrieving revision 1.86
diff -c -r1.86 parse_oper.c
*** parse_oper.c        14 Mar 2006 22:48:21 -0000      1.86
--- parse_oper.c        24 Mar 2006 18:59:58 -0000
***************
*** 43,48 ****
--- 43,49 ----
  static Expr *make_op_expr(ParseState *pstate, Operator op,
                         Node *ltree, Node *rtree,
                         Oid ltypeId, Oid rtypeId);
+ static List * mk_oper_arg_list( Oid arg);
  
  
  /*
***************
*** 388,431 ****
   */
  static Oid
  binary_oper_exact(Oid arg1, Oid arg2,
!                                 FuncCandidateList candidates)
  {
!       FuncCandidateList cand;
!       bool            was_unknown = false;
  
        /* Unspecified type for one of the arguments? then use the other */
        if ((arg1 == UNKNOWNOID) && (arg2 != InvalidOid))
        {
                arg1 = arg2;
!               was_unknown = true;
        }
        else if ((arg2 == UNKNOWNOID) && (arg1 != InvalidOid))
        {
                arg2 = arg1;
!               was_unknown = true;
        }
  
!       for (cand = candidates; cand != NULL; cand = cand->next)
!       {
!               if (arg1 == cand->args[0] && arg2 == cand->args[1])
!                       return cand->oid;
!       }
  
!       if (was_unknown)
        {
!               /* arg1 and arg2 are the same here, need only look at arg1 */
!               Oid                     basetype = getBaseType(arg1);
! 
!               if (basetype != arg1)
                {
                        for (cand = candidates; cand != NULL; cand = cand->next)
                        {
!                               if (basetype == cand->args[0] && basetype == 
cand->args[1])
                                        return cand->oid;
                        }
                }
        }
  
        return InvalidOid;
  }
  
--- 389,455 ----
   */
  static Oid
  binary_oper_exact(Oid arg1, Oid arg2,
!                       FuncCandidateList candidates)
  {
!       FuncCandidateList       cand;
!       int                     was_unknown = 0;
!       List *          larglist=NIL;
!       List *          rarglist=NIL;
!       ListCell *      larg_cell;
!       ListCell *      rarg_cell;
!       Oid                     larg = InvalidOid;
!       Oid                     rarg = InvalidOid;
  
+ 
+       /* Check for both args == UNKNOWNOID is in oper() */
        /* Unspecified type for one of the arguments? then use the other */
        if ((arg1 == UNKNOWNOID) && (arg2 != InvalidOid))
        {
                arg1 = arg2;
!               was_unknown = 1;
        }
        else if ((arg2 == UNKNOWNOID) && (arg1 != InvalidOid))
        {
                arg2 = arg1;
!               was_unknown = 2;
        }
  
!       /*
!       * create argument list of type + basetypes
!       */
!       larglist = mk_oper_arg_list( arg1);
!       rarglist = mk_oper_arg_list( arg2);
  
!       /*
!       * Check each basetype combination for exact match
!       * This favors current types and basetypes for matches
!       */
!       foreach ( larg_cell, larglist )
        {
!               larg = lfirst_oid( larg_cell );
!               foreach ( rarg_cell, rarglist )
                {
+                       rarg = lfirst_oid( rarg_cell );
                        for (cand = candidates; cand != NULL; cand = cand->next)
                        {
!                               if (larg == cand->args[0] && rarg == 
cand->args[1])
                                        return cand->oid;
                        }
                }
        }
  
+       /*
+       * Put back the unknownid to let func sel handle it
+       */
+       if (was_unknown == 1) 
+       {
+               arg1 = UNKNOWNOID;
+       }
+       else if (was_unknown == 2)
+       {
+               arg2 = UNKNOWNOID;
+       }
+ 
        return InvalidOid;
  }
  
***************
*** 518,531 ****
                /*
                 * Check for an "exact" match.
                 */
!               operOid = binary_oper_exact(ltypeId, rtypeId, clist);
                if (!OidIsValid(operOid))
                {
                        /*
-                        * Otherwise, search for the most suitable candidate.
-                        */
- 
-                       /*
                         * Unspecified type for one of the arguments? then use 
the other
                         * (XXX this is probably dead code?)
                         */
--- 542,554 ----
                /*
                 * Check for an "exact" match.
                 */
!               if ( ltypeId == UNKNOWNOID && rtypeId == UNKNOWNOID )
!                       operOid = InvalidOid;
!               else
!                       operOid = binary_oper_exact(ltypeId, rtypeId, clist);
                if (!OidIsValid(operOid))
                {
                        /*
                         * Unspecified type for one of the arguments? then use 
the other
                         * (XXX this is probably dead code?)
                         */
***************
*** 533,538 ****
--- 556,565 ----
                                rtypeId = ltypeId;
                        else if (ltypeId == InvalidOid)
                                ltypeId = rtypeId;
+ 
+                       /*
+                        * search for the most suitable candidate.
+                        */
                        inputOids[0] = ltypeId;
                        inputOids[1] = rtypeId;
                        fdresult = oper_select_candidate(2, inputOids, clist, 
&operOid);
***************
*** 1017,1019 ****
--- 1044,1074 ----
  
        return (Expr *) result;
  }
+ 
+ /*
+  * mk_oper_arg_list()
+  *    Build type argument list of current type and basetypes.
+  *
+  * Order of list is important.  Current type has precedence,
+  * then parent types in order of distance from current type.
+  *
+  * Called by binary_oper_exact to find operators for domains.
+  */
+ List *
+ mk_oper_arg_list( Oid arg)
+ {
+       List *arglist = NIL;
+       Oid  basetype = InvalidOid;
+       Oid  prevtype = arg;
+ 
+ 
+       arglist = lappend_oid( arglist, prevtype );
+       basetype = getBaseType(prevtype);
+       while ( basetype != prevtype ){
+               arglist = lappend_oid( arglist, basetype );
+               prevtype = basetype;
+               basetype = getBaseType(prevtype);
+       }
+       return arglist;
+ }
+ 
---------------------------(end of broadcast)---------------------------
TIP 2: Don't 'kill -9' the postmaster

Reply via email to