Thread: [HACKERS] Domains and arrays and composites, oh my

[HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
I started to look into allowing domains over composite types, which is
another never-implemented case that there's no very good reason not to
allow.  Well, other than the argument that the SQL standard only allows
domains over "predefined" (built-in) types ... but we blew past that
restriction ages ago.

Any thought that there might be some fundamental problem with that was
soon dispelled when I noticed that we allow domains over arrays of
composite types.  Ahem.

They even work, mostly.  I wrote a few test cases and couldn't find
anything that failed except for attempts to insert or update multiple
subfields of the same base column; that's because process_matched_tle()
fails to look through CoerceToDomain nodes.  But that turns out to be a
bug even for the simpler case of domains over arrays of scalars, which
is certainly supported.  That is, given

create domain domainint4arr int4[];
create table domarrtest (testint4arr domainint4arr);

this ought to work:

insert into domarrtest (testint4arr[1], testint4arr[3]) values (11,22);

It would work with a plain-array target column, but with the domain
it fails with

ERROR:  multiple assignments to same column "testint4arr"

Hence, the attached proposed patch, which fixes that oversight and
adds some relevant test cases.  (I've not yet looked to see if any
documentation changes would be appropriate.)

I think this is a bug fix and should be back-patched.  Any objections?

            regards, tom lane

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f3c7526..0a6f4f3 100644
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** process_matched_tle(TargetEntry *src_tle
*** 934,939 ****
--- 934,940 ----
                      const char *attrName)
  {
      TargetEntry *result;
+     CoerceToDomain *coerce_expr = NULL;
      Node       *src_expr;
      Node       *prior_expr;
      Node       *src_input;
*************** process_matched_tle(TargetEntry *src_tle
*** 970,979 ****
--- 971,1000 ----
       * For FieldStore, instead of nesting we can generate a single
       * FieldStore with multiple target fields.  We must nest when
       * ArrayRefs are involved though.
+      *
+      * As a further complication, the destination column might be a domain,
+      * resulting in each assignment containing a CoerceToDomain node over a
+      * FieldStore or ArrayRef.  These should have matching target domains, so
+      * we strip them and reconstitute a single CoerceToDomain over the
+      * combined FieldStore/ArrayRef nodes.  (Notice that this has the result
+      * that the domain's checks are applied only after we do all the field or
+      * element updates, not after each one.  This is arguably desirable.)
       *----------
       */
      src_expr = (Node *) src_tle->expr;
      prior_expr = (Node *) prior_tle->expr;
+
+     if (src_expr && IsA(src_expr, CoerceToDomain) &&
+         prior_expr && IsA(prior_expr, CoerceToDomain) &&
+         ((CoerceToDomain *) src_expr)->resulttype ==
+         ((CoerceToDomain *) prior_expr)->resulttype)
+     {
+         /* we assume without checking that resulttypmod/resultcollid match */
+         coerce_expr = (CoerceToDomain *) src_expr;
+         src_expr = (Node *) ((CoerceToDomain *) src_expr)->arg;
+         prior_expr = (Node *) ((CoerceToDomain *) prior_expr)->arg;
+     }
+
      src_input = get_assignment_input(src_expr);
      prior_input = get_assignment_input(prior_expr);
      if (src_input == NULL ||
*************** process_matched_tle(TargetEntry *src_tle
*** 1042,1047 ****
--- 1063,1078 ----
          newexpr = NULL;
      }

+     if (coerce_expr)
+     {
+         /* put back the CoerceToDomain */
+         CoerceToDomain *newcoerce = makeNode(CoerceToDomain);
+
+         memcpy(newcoerce, coerce_expr, sizeof(CoerceToDomain));
+         newcoerce->arg = (Expr *) newexpr;
+         newexpr = (Node *) newcoerce;
+     }
+
      result = flatCopyTargetEntry(src_tle);
      result->expr = (Expr *) newexpr;
      return result;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 41b70e2..9fb750d 100644
*** a/src/test/regress/expected/domain.out
--- b/src/test/regress/expected/domain.out
*************** INSERT INTO domarrtest values ('{2,2}',
*** 107,112 ****
--- 107,113 ----
  INSERT INTO domarrtest values (NULL, '{{"a","b","c"},{"d","e","f"}}');
  INSERT INTO domarrtest values (NULL, '{{"toolong","b","c"},{"d","e","f"}}');
  ERROR:  value too long for type character varying(4)
+ INSERT INTO domarrtest (testint4arr[1], testint4arr[3]) values (11,22);
  select * from domarrtest;
    testint4arr  |    testchar4arr
  ---------------+---------------------
*************** select * from domarrtest;
*** 115,121 ****
   {2,2}         | {{a,b},{c,d},{e,f}}
   {2,2}         | {{a},{c}}
                 | {{a,b,c},{d,e,f}}
! (5 rows)

  select testint4arr[1], testchar4arr[2:2] from domarrtest;
   testint4arr | testchar4arr
--- 116,123 ----
   {2,2}         | {{a,b},{c,d},{e,f}}
   {2,2}         | {{a},{c}}
                 | {{a,b,c},{d,e,f}}
!  {11,NULL,22}  |
! (6 rows)

  select testint4arr[1], testchar4arr[2:2] from domarrtest;
   testint4arr | testchar4arr
*************** select testint4arr[1], testchar4arr[2:2]
*** 125,131 ****
             2 | {{c,d}}
             2 | {{c}}
               | {{d,e,f}}
! (5 rows)

  select array_dims(testint4arr), array_dims(testchar4arr) from domarrtest;
   array_dims | array_dims
--- 127,134 ----
             2 | {{c,d}}
             2 | {{c}}
               | {{d,e,f}}
!           11 |
! (6 rows)

  select array_dims(testint4arr), array_dims(testchar4arr) from domarrtest;
   array_dims | array_dims
*************** select array_dims(testint4arr), array_di
*** 135,141 ****
   [1:2]      | [1:3][1:2]
   [1:2]      | [1:2][1:1]
              | [1:2][1:3]
! (5 rows)

  COPY domarrtest FROM stdin;
  COPY domarrtest FROM stdin;    -- fail
--- 138,145 ----
   [1:2]      | [1:3][1:2]
   [1:2]      | [1:2][1:1]
              | [1:2][1:3]
!  [1:3]      |
! (6 rows)

  COPY domarrtest FROM stdin;
  COPY domarrtest FROM stdin;    -- fail
*************** select * from domarrtest;
*** 149,157 ****
   {2,2}         | {{a,b},{c,d},{e,f}}
   {2,2}         | {{a},{c}}
                 | {{a,b,c},{d,e,f}}
   {3,4}         | {q,w,e}
                 |
! (7 rows)

  drop table domarrtest;
  drop domain domainint4arr restrict;
--- 153,173 ----
   {2,2}         | {{a,b},{c,d},{e,f}}
   {2,2}         | {{a},{c}}
                 | {{a,b,c},{d,e,f}}
+  {11,NULL,22}  |
   {3,4}         | {q,w,e}
                 |
! (8 rows)
!
! update domarrtest set
!   testint4arr[1] = testint4arr[1] + 1,
!   testint4arr[3] = testint4arr[3] - 1
! where testchar4arr is null;
! select * from domarrtest where testchar4arr is null;
!    testint4arr    | testchar4arr
! ------------------+--------------
!  {12,NULL,21}     |
!  {NULL,NULL,NULL} |
! (2 rows)

  drop table domarrtest;
  drop domain domainint4arr restrict;
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 182,187 ****
--- 198,289 ----
  (1 row)

  drop domain dia;
+ -- Test domains over arrays of composite
+ create type comptype as (r float8, i float8);
+ create domain dcomptypea as comptype[];
+ create table dcomptable (d1 dcomptypea unique);
+ insert into dcomptable values (array[row(1,2)]::dcomptypea);
+ insert into dcomptable values (array[row(3,4), row(5,6)]::comptype[]);
+ insert into dcomptable values (array[row(7,8)::comptype, row(9,10)::comptype]);
+ insert into dcomptable values (array[row(1,2)]::dcomptypea);  -- fail on uniqueness
+ ERROR:  duplicate key value violates unique constraint "dcomptable_d1_key"
+ DETAIL:  Key (d1)=({"(1,2)"}) already exists.
+ insert into dcomptable (d1[1]) values(row(9,10));
+ insert into dcomptable (d1[1].r) values(11);
+ select * from dcomptable;
+          d1
+ --------------------
+  {"(1,2)"}
+  {"(3,4)","(5,6)"}
+  {"(7,8)","(9,10)"}
+  {"(9,10)"}
+  {"(11,)"}
+ (5 rows)
+
+ select d1[2], d1[1].r, d1[1].i from dcomptable;
+    d1   | r  | i
+ --------+----+----
+         |  1 |  2
+  (5,6)  |  3 |  4
+  (9,10) |  7 |  8
+         |  9 | 10
+         | 11 |
+ (5 rows)
+
+ update dcomptable set d1[2] = row(d1[2].i, d1[2].r);
+ select * from dcomptable;
+          d1
+ --------------------
+  {"(1,2)","(,)"}
+  {"(3,4)","(6,5)"}
+  {"(7,8)","(10,9)"}
+  {"(9,10)","(,)"}
+  {"(11,)","(,)"}
+ (5 rows)
+
+ update dcomptable set d1[1].r = d1[1].r + 1 where d1[1].i > 0;
+ select * from dcomptable;
+          d1
+ --------------------
+  {"(11,)","(,)"}
+  {"(2,2)","(,)"}
+  {"(4,4)","(6,5)"}
+  {"(8,8)","(10,9)"}
+  {"(10,10)","(,)"}
+ (5 rows)
+
+ alter domain dcomptypea add constraint c1 check (value[1].r <= value[1].i);
+ alter domain dcomptypea add constraint c2 check (value[1].r > value[1].i);  -- fail
+ ERROR:  column "d1" of table "dcomptable" contains values that violate the new constraint
+ select array[row(2,1)]::dcomptypea;  -- fail
+ ERROR:  value for domain dcomptypea violates check constraint "c1"
+ insert into dcomptable values (array[row(1,2)]::comptype[]);
+ insert into dcomptable values (array[row(2,1)]::comptype[]);  -- fail
+ ERROR:  value for domain dcomptypea violates check constraint "c1"
+ insert into dcomptable (d1[1].r) values(99);
+ insert into dcomptable (d1[1].r, d1[1].i) values(99, 100);
+ insert into dcomptable (d1[1].r, d1[1].i) values(100, 99);  -- fail
+ ERROR:  value for domain dcomptypea violates check constraint "c1"
+ update dcomptable set d1[1].r = d1[1].r + 1 where d1[1].i > 0;  -- fail
+ ERROR:  value for domain dcomptypea violates check constraint "c1"
+ update dcomptable set d1[1].r = d1[1].r - 1 where d1[1].i > 0;
+ select * from dcomptable;
+          d1
+ --------------------
+  {"(11,)","(,)"}
+  {"(99,)"}
+  {"(1,2)","(,)"}
+  {"(3,4)","(6,5)"}
+  {"(7,8)","(10,9)"}
+  {"(9,10)","(,)"}
+  {"(0,2)"}
+  {"(98,100)"}
+ (8 rows)
+
+ drop table dcomptable;
+ drop type comptype cascade;
+ NOTICE:  drop cascades to type dcomptypea
+ -- Test not-null restrictions
  create domain dnotnull varchar(15) NOT NULL;
  create domain dnull    varchar(15);
  create domain dcheck   varchar(15) NOT NULL CHECK (VALUE = 'a' OR VALUE = 'c' OR VALUE = 'd');
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 407d3ef..1fd7b11 100644
*** a/src/test/regress/sql/domain.sql
--- b/src/test/regress/sql/domain.sql
*************** INSERT INTO domarrtest values ('{2,2}',
*** 85,90 ****
--- 85,91 ----
  INSERT INTO domarrtest values ('{2,2}', '{{"a"},{"c"}}');
  INSERT INTO domarrtest values (NULL, '{{"a","b","c"},{"d","e","f"}}');
  INSERT INTO domarrtest values (NULL, '{{"toolong","b","c"},{"d","e","f"}}');
+ INSERT INTO domarrtest (testint4arr[1], testint4arr[3]) values (11,22);
  select * from domarrtest;
  select testint4arr[1], testchar4arr[2:2] from domarrtest;
  select array_dims(testint4arr), array_dims(testchar4arr) from domarrtest;
*************** COPY domarrtest FROM stdin;    -- fail
*** 100,105 ****
--- 101,113 ----

  select * from domarrtest;

+ update domarrtest set
+   testint4arr[1] = testint4arr[1] + 1,
+   testint4arr[3] = testint4arr[3] - 1
+ where testchar4arr is null;
+
+ select * from domarrtest where testchar4arr is null;
+
  drop table domarrtest;
  drop domain domainint4arr restrict;
  drop domain domainchar4arr restrict;
*************** select pg_typeof('{1,2,3}'::dia);
*** 111,116 ****
--- 119,164 ----
  select pg_typeof('{1,2,3}'::dia || 42); -- should be int[] not dia
  drop domain dia;

+
+ -- Test domains over arrays of composite
+
+ create type comptype as (r float8, i float8);
+ create domain dcomptypea as comptype[];
+ create table dcomptable (d1 dcomptypea unique);
+
+ insert into dcomptable values (array[row(1,2)]::dcomptypea);
+ insert into dcomptable values (array[row(3,4), row(5,6)]::comptype[]);
+ insert into dcomptable values (array[row(7,8)::comptype, row(9,10)::comptype]);
+ insert into dcomptable values (array[row(1,2)]::dcomptypea);  -- fail on uniqueness
+ insert into dcomptable (d1[1]) values(row(9,10));
+ insert into dcomptable (d1[1].r) values(11);
+
+ select * from dcomptable;
+ select d1[2], d1[1].r, d1[1].i from dcomptable;
+ update dcomptable set d1[2] = row(d1[2].i, d1[2].r);
+ select * from dcomptable;
+ update dcomptable set d1[1].r = d1[1].r + 1 where d1[1].i > 0;
+ select * from dcomptable;
+
+ alter domain dcomptypea add constraint c1 check (value[1].r <= value[1].i);
+ alter domain dcomptypea add constraint c2 check (value[1].r > value[1].i);  -- fail
+
+ select array[row(2,1)]::dcomptypea;  -- fail
+ insert into dcomptable values (array[row(1,2)]::comptype[]);
+ insert into dcomptable values (array[row(2,1)]::comptype[]);  -- fail
+ insert into dcomptable (d1[1].r) values(99);
+ insert into dcomptable (d1[1].r, d1[1].i) values(99, 100);
+ insert into dcomptable (d1[1].r, d1[1].i) values(100, 99);  -- fail
+ update dcomptable set d1[1].r = d1[1].r + 1 where d1[1].i > 0;  -- fail
+ update dcomptable set d1[1].r = d1[1].r - 1 where d1[1].i > 0;
+ select * from dcomptable;
+
+ drop table dcomptable;
+ drop type comptype cascade;
+
+
+ -- Test not-null restrictions
+
  create domain dnotnull varchar(15) NOT NULL;
  create domain dnull    varchar(15);
  create domain dcheck   varchar(15) NOT NULL CHECK (VALUE = 'a' OR VALUE = 'c' OR VALUE = 'd');

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
I wrote:
> I started to look into allowing domains over composite types, which is
> another never-implemented case that there's no very good reason not to
> allow.  Well, other than the argument that the SQL standard only allows
> domains over "predefined" (built-in) types ... but we blew past that
> restriction ages ago.

Attached is a draft patch that allows domains over composite types.
I think it's probably complete on its own terms, but there are some
questions around behavior of functions returning domain-over-composite
that could use discussion, and some of the PLs need some follow-on work.

The core principle here was to not modify execution-time behavior by
adding domain checks to existing code paths.  Rather, I wanted the
parser to insert CoerceToDomain nodes wherever checks would be needed.
Row-returning node types such as RowExpr and FieldStore only return
regular composite types, as before; to generate a value of a composite
domain, we construct a value of the base type and then CoerceToDomain.
(This is pretty analogous to what happens for domains over arrays.)
Whole-row Vars can only have regular composite types as vartype,
TupleDescs can only have regular composite types as tdtypeid, composite
Datums only have regular composite type OIDs in datum_typeid.  (The last
would be expected anyway, since the physical representation of a domain
value is never different from that of its base type.)

Doing it that way led to a nicely small patch, only about 160 net new
lines of code.  However, not touching the executor meant not touching
the behavior of functions that return domains, even if the domain is
domain-over-composite.  In code terms this means that 
get_call_result_type() and sibling functions will return TYPEFUNC_SCALAR
not TYPEFUNC_COMPOSITE for such a result type.  The difficulty with
changing that is that if these functions look through the domain, then
the calling code (in, usually, a PL) will simply build and return a result
of the underlying composite type, failing to apply any domain constraints.
Trying to get out-of-core PLs on board with a change in those requirements
seems like a risky proposition.

Concretely, consider

create type complex as (r float8, i float8);
create domain dcomplex as complex;

You can make a SQL-language function to return complex in either of two
ways:

create function fc() returns complex language sql
as 'select 1.0::float8, 2.0::float8';

create function fc() returns complex language sql
as 'select row(1.0::float8, 2.0::float8)::complex';

As the patch stands, though, only the second way works for domains over
composite:

regression=# create function fdc() returns dcomplex language sql
as 'select 1.0::float8, 2.0::float8';
ERROR:  return type mismatch in function declared to return dcomplex
DETAIL:  Final statement must return exactly one column.
CONTEXT:  SQL function "fdc"
regression=# create function fdc() returns dcomplex language sql
as 'select row(1.0::float8, 2.0)::dcomplex';
CREATE FUNCTION

Now, maybe that's fine.  SQL-language functions have never been very
willing to insert implicit casts to get to the function result type,
and certainly the only way that the first definition could be legal
is if there were an implicit up-cast to the domain type.  It might be
OK to just leave it like this, though some documentation about it
would be a good idea.

plpgsql functions seem generally okay as far as composite domain return
types go, because they don't have anything corresponding to the row
return convention of SQL functions.  And plpgsql's greater willingness
to do implicit coercions reduces the notational burden, too.  But
there's some work yet to be done to get plpgsql to realize that
composite domain local variables have substructure.  For example,
this works:

    declare x complex;
    ...
    x.r := 1;

but it fails if x is dcomplex.  But ISTM that that would be better
handled as a followon feature patch.  I suspect that the other PLs may
have similar issues where it'd be nice to allow domain-over-composite
to act like a plain composite for specific purposes; but I've not looked.

Another issue related to function result types is that the parser
considers a function-in-FROM returning a composite domain to be
producing a scalar result not a rowtype.  Thus you get this for a
function returning complex:

regression=# select * from fc();
 r | i 
---+---
 1 | 2
(1 row)

but this for a function returning dcomplex:

regression=# select * from fdc();
  fdc  
-------
 (1,2)
(1 row)

I think that that could be changed with only local changes in parse
analysis, but do we want to change it?  Arguably, making fdc() act the
same as fc() here would amount to implicitly downcasting the domain to
its base type.  But doing so here is optional, not necessary in order to
make the statement sane at all, and it's arguable that we shouldn't do
that if the user didn't tell us to.  A user who does want that to happen
can downcast explicitly:

regression=# select * from cast(fdc() as complex);
 r | i 
---+---
 1 | 2
(1 row)

(For arcane syntactic reasons you can't abbreviate CAST with :: here.)
Another point is that if you do want the domain value as a domain
value, and not smashed to its base type, it would be hard to get at
if the parser acts this way --- "foo.*" would end up producing the base
rowtype, or if it didn't, we'd have some issues with the previously
noted rule about whole-row Vars never having domain types.

So there's a case to be made that this behavior is fine as-is, but
certainly you could also argue that it's a POLA violation.

Digression: one reason I'm hesitant to introduce inessential reductions
of domains to base types is that I'm looking ahead to arrays over
domains, which will provide a workaround for the people who complain
that they wish 2-D arrays would work type-wise like arrays of 1-D array
objects.  If you "create domain inta as int[]" then inta[] would act
like an array of array objects, mostly solving the problem I think.
But it solves the problem only because we don't consider that a domain
is indistinguishable from its base type.  It's hard to be sure without
having done the work yet, but I think there will be cases where being
over-eager to treat a domain as its base type might break the behavior
we want for that case.  So I don't want to create a precedent for that
here.

Thoughts?

Obviously this is work for v11; I'll go stick it in the next commitfest.

            regards, tom lane

diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 245a374..1bd8a58 100644
*** a/src/backend/catalog/pg_inherits.c
--- b/src/backend/catalog/pg_inherits.c
*************** has_superclass(Oid relationId)
*** 301,306 ****
--- 301,311 ----
  /*
   * Given two type OIDs, determine whether the first is a complex type
   * (class type) that inherits from the second.
+  *
+  * This essentially asks whether the first type is guaranteed to be coercible
+  * to the second.  Therefore, we allow the first type to be a domain over a
+  * complex type that inherits from the second; that creates no difficulties.
+  * But the second type cannot be a domain.
   */
  bool
  typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId)
*************** typeInheritsFrom(Oid subclassTypeId, Oid
*** 314,322 ****
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeidTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
--- 319,327 ----
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeOrDomainTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type or domain over one */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 571856e..47916cf 100644
*** a/src/backend/catalog/pg_proc.c
--- b/src/backend/catalog/pg_proc.c
*************** ProcedureCreate(const char *procedureNam
*** 262,268 ****
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeidTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
--- 262,268 ----
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeOrDomainTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index c2fc59d..0a08ef5 100644
*** a/src/backend/commands/typecmds.c
--- b/src/backend/commands/typecmds.c
*************** DefineDomain(CreateDomainStmt *stmt)
*** 796,808 ****
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, another domain, an enum or a range
!      * type. Domains over pseudotypes would create a security hole.  Domains
!      * over composite types might be made to work in the future, but not
!      * today.
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
--- 796,811 ----
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, a composite type, another domain,
!      * an enum or a range type.  Domains over pseudotypes would create a
!      * security hole.  (It would be shorter to code this to just check for
!      * pseudotypes; but it seems safer to call out the specific typtypes that
!      * are supported, rather than assume that all future typtypes would be
!      * automatically supported.)
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
+         typtype != TYPTYPE_COMPOSITE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 0bc7dba..7b16d6c 100644
*** a/src/backend/parser/parse_coerce.c
--- b/src/backend/parser/parse_coerce.c
*************** coerce_type(ParseState *pstate, Node *no
*** 500,508 ****
--- 500,525 ----
           * Input class type is a subclass of target, so generate an
           * appropriate runtime conversion (removing unneeded columns and
           * possibly rearranging the ones that are wanted).
+          *
+          * We will also get here when the input is a domain over a subclass of
+          * the target type.  To keep life simple for the executor, we define
+          * ConvertRowtypeExpr as only working between regular composite types;
+          * therefore, in such cases insert a RelabelType to smash the input
+          * expression down to its base type.
           */
+         Oid            baseTypeId = getBaseType(inputTypeId);
          ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);

+         if (baseTypeId != inputTypeId)
+         {
+             RelabelType *rt = makeRelabelType((Expr *) node,
+                                               baseTypeId, -1,
+                                               InvalidOid,
+                                               COERCE_IMPLICIT_CAST);
+
+             rt->location = location;
+             node = (Node *) rt;
+         }
          r->arg = (Expr *) node;
          r->resulttype = targetTypeId;
          r->convertformat = cformat;
*************** coerce_record_to_complex(ParseState *pst
*** 938,943 ****
--- 955,962 ----
                           int location)
  {
      RowExpr    *rowexpr;
+     Oid            baseTypeId;
+     int32        baseTypeMod = -1;
      TupleDesc    tupdesc;
      List       *args = NIL;
      List       *newargs;
*************** coerce_record_to_complex(ParseState *pst
*** 973,979 ****
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     tupdesc = lookup_rowtype_tupdesc(targetTypeId, -1);
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
--- 992,1005 ----
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     /*
!      * Look up the composite type, accounting for possibility that what we are
!      * given is a domain over composite.
!      */
!     baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!     tupdesc = lookup_rowtype_tupdesc(baseTypeId, baseTypeMod);
!
!     /* Process the fields */
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
*************** coerce_record_to_complex(ParseState *pst
*** 1041,1050 ****

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = targetTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
      return (Node *) rowexpr;
  }

--- 1067,1087 ----

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = baseTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
+
+     /* If target is a domain, apply constraints */
+     if (baseTypeId != targetTypeId)
+     {
+         rowexpr->row_format = COERCE_IMPLICIT_CAST;
+         return coerce_to_domain((Node *) rowexpr,
+                                 baseTypeId, baseTypeMod,
+                                 targetTypeId,
+                                 cformat, location, true, false);
+     }
+
      return (Node *) rowexpr;
  }

*************** is_complex_array(Oid typid)
*** 2379,2391 ****

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId.  (This is conceptually similar to the subtype
!  * relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeidTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
--- 2416,2428 ----

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId, or is a domain over such a row type.  (This is conceptually
!  * similar to the subtype relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeOrDomainTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 8487eda..7570108 100644
*** a/src/backend/parser/parse_func.c
--- b/src/backend/parser/parse_func.c
*************** ParseComplexProjection(ParseState *pstat
*** 1819,1836 ****
      }

      /*
!      * Else do it the hard way with get_expr_result_type().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else if (get_expr_result_type(first_arg, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          return NULL;            /* unresolvable RECORD type */
-     Assert(tupdesc);

      for (i = 0; i < tupdesc->natts; i++)
      {
--- 1819,1837 ----
      }

      /*
!      * Else do it the hard way with get_expr_result_tupdesc().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else
!         tupdesc = get_expr_result_tupdesc(first_arg, true);
!     if (!tupdesc)
          return NULL;            /* unresolvable RECORD type */

      for (i = 0; i < tupdesc->natts; i++)
      {
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 0a70539..d689e2a 100644
*** a/src/backend/parser/parse_target.c
--- b/src/backend/parser/parse_target.c
*************** transformAssignmentIndirection(ParseStat
*** 725,730 ****
--- 725,732 ----
          else
          {
              FieldStore *fstore;
+             Oid            baseTypeId;
+             int32        baseTypeMod;
              Oid            typrelid;
              AttrNumber    attnum;
              Oid            fieldTypeId;
*************** transformAssignmentIndirection(ParseStat
*** 752,758 ****

              /* No subscripts, so can process field selection here */

!             typrelid = typeidTypeRelid(targetTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
--- 754,767 ----

              /* No subscripts, so can process field selection here */

!             /*
!              * Look up the composite type, accounting for possibility that
!              * what we are given is a domain over composite.
!              */
!             baseTypeMod = targetTypMod;
!             baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!
!             typrelid = typeidTypeRelid(baseTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
*************** transformAssignmentIndirection(ParseStat
*** 796,802 ****
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = targetTypeId;

              return (Node *) fstore;
          }
--- 805,819 ----
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = baseTypeId;
!
!             /* If target is a domain, apply constraints */
!             if (baseTypeId != targetTypeId)
!                 return coerce_to_domain((Node *) fstore,
!                                         baseTypeId, baseTypeMod,
!                                         targetTypeId,
!                                         COERCE_IMPLICIT_CAST, location,
!                                         false, false);

              return (Node *) fstore;
          }
*************** ExpandRowReference(ParseState *pstate, N
*** 1385,1406 ****
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.  We use
!      * get_expr_result_type() because that can handle references to functions
!      * returning anonymous record types.  If that fails, use
!      * lookup_rowtype_tupdesc(), which will almost certainly fail as well, but
!      * it will give an appropriate error message.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
--- 1402,1419 ----
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.
!      * get_expr_result_tupdesc() handles this conveniently.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else
!         tupleDesc = get_expr_result_tupdesc(expr, false);
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
*************** expandRecordVariable(ParseState *pstate,
*** 1608,1622 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!
!     return tupleDesc;
  }


--- 1621,1629 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     return get_expr_result_tupdesc(expr, false);
  }


diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index d0b3fbe..b032651 100644
*** a/src/backend/parser/parse_type.c
--- b/src/backend/parser/parse_type.c
*************** stringTypeDatum(Type tp, char *string, i
*** 641,647 ****
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /* given a typeid, return the type's typrelid (associated relation, if any) */
  Oid
  typeidTypeRelid(Oid type_id)
  {
--- 641,650 ----
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /*
!  * Given a typeid, return the type's typrelid (associated relation), if any.
!  * Returns InvalidOid if type is not a composite type.
!  */
  Oid
  typeidTypeRelid(Oid type_id)
  {
*************** typeidTypeRelid(Oid type_id)
*** 652,658 ****
      typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
      if (!HeapTupleIsValid(typeTuple))
          elog(ERROR, "cache lookup failed for type %u", type_id);
-
      type = (Form_pg_type) GETSTRUCT(typeTuple);
      result = type->typrelid;
      ReleaseSysCache(typeTuple);
--- 655,660 ----
*************** typeidTypeRelid(Oid type_id)
*** 660,665 ****
--- 662,699 ----
  }

  /*
+  * Given a typeid, return the type's typrelid (associated relation), if any.
+  * Returns InvalidOid if type is not a composite type or a domain over one.
+  * This is the same as typeidTypeRelid(getBaseType(type_id)), but faster.
+  */
+ Oid
+ typeOrDomainTypeRelid(Oid type_id)
+ {
+     HeapTuple    typeTuple;
+     Form_pg_type type;
+     Oid            result;
+
+     for (;;)
+     {
+         typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
+         if (!HeapTupleIsValid(typeTuple))
+             elog(ERROR, "cache lookup failed for type %u", type_id);
+         type = (Form_pg_type) GETSTRUCT(typeTuple);
+         if (type->typtype != TYPTYPE_DOMAIN)
+         {
+             /* Not a domain, so done looking through domains */
+             break;
+         }
+         /* It is a domain, so examine the base type instead */
+         type_id = type->typbasetype;
+         ReleaseSysCache(typeTuple);
+     }
+     result = type->typrelid;
+     ReleaseSysCache(typeTuple);
+     return result;
+ }
+
+ /*
   * error context callback for parse failure during parseTypeString()
   */
  static void
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5a4adbd..9d069b5 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** get_name_for_var_field(Var *var, int fie
*** 6706,6722 ****

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_type. If that fails, we try
!      * lookup_rowtype_tupdesc, which will probably fail too, but will ereport
!      * an acceptable message.
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         if (get_expr_result_type((Node *) var, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!             tupleDesc = lookup_rowtype_tupdesc_copy(exprType((Node *) var),
!                                                     exprTypmod((Node *) var));
!         Assert(tupleDesc);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(tupleDesc->attrs[fieldno - 1]->attname);
--- 6706,6717 ----

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_tupdesc().
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         tupleDesc = get_expr_result_tupdesc((Node *) var, false);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(tupleDesc->attrs[fieldno - 1]->attname);
*************** get_name_for_var_field(Var *var, int fie
*** 7019,7032 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!     Assert(tupleDesc);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(tupleDesc->attrs[fieldno - 1]->attname);
--- 7014,7022 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     tupleDesc = get_expr_result_tupdesc(expr, false);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(tupleDesc->attrs[fieldno - 1]->attname);
diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c
index be47d41..f36be55 100644
*** a/src/backend/utils/fmgr/funcapi.c
--- b/src/backend/utils/fmgr/funcapi.c
*************** internal_get_result_type(Oid funcid,
*** 394,399 ****
--- 394,474 ----
  }

  /*
+  * get_expr_result_tupdesc
+  *        Get a tupdesc describing the result of a composite-valued expression
+  *
+  * If expression is not composite or rowtype can't be determined, returns NULL
+  * if noError is true, else throws error.
+  *
+  * This can resolve the same cases that get_expr_result_type() can, plus one
+  * more: if the expression yields domain-over-composite, it will look through
+  * the domain and return the base type's tupdesc.  This is generally *not*
+  * appropriate to use when the caller is responsible for constructing a value
+  * of the expression's type, since one would end up constructing a value of
+  * the base type and failing to apply domain constraints if any.  However,
+  * when the goal is just to find out what fields can be extracted from the
+  * expression's value, this is a convenient helper function.
+  */
+ TupleDesc
+ get_expr_result_tupdesc(Node *expr, bool noError)
+ {
+     TupleDesc    tupleDesc;
+     TupleDesc    cache_tupdesc;
+     Oid            exprTypeId;
+     Oid            baseTypeId;
+     int32        baseTypeMod;
+
+     /*
+      * First, try get_expr_result_type(), because that can handle references
+      * to functions returning anonymous record types.
+      */
+     if (get_expr_result_type(expr, NULL, &tupleDesc) == TYPEFUNC_COMPOSITE)
+         return tupleDesc;
+
+     /*
+      * There is one case get_expr_result_type() doesn't handle that we should,
+      * which is a domain over composite.  So drill through any domain type
+      * before asking lookup_rowtype_tupdesc_noerror().
+      */
+     exprTypeId = exprType(expr);
+     baseTypeMod = exprTypmod(expr);
+     baseTypeId = getBaseTypeAndTypmod(exprTypeId, &baseTypeMod);
+
+     cache_tupdesc = lookup_rowtype_tupdesc_noerror(baseTypeId, baseTypeMod,
+                                                    true);
+     if (cache_tupdesc)
+     {
+         /*
+          * Success!  But caller isn't expecting to have to manage a tupdesc
+          * refcount, so copy the cached tupdesc.
+          */
+         tupleDesc = CreateTupleDescCopyConstr(cache_tupdesc);
+         DecrTupleDescRefCount(cache_tupdesc);
+         return tupleDesc;
+     }
+
+     /*
+      * Throw error if requested.  We could have left this to
+      * lookup_rowtype_tupdesc_noerror, but if we're dealing with a domain
+      * type, we prefer to finger the domain not its base type.
+      */
+     if (!noError)
+     {
+         if (exprTypeId != RECORDOID)
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("type %s is not composite",
+                             format_type_be(exprTypeId))));
+         else
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("record type has not been registered")));
+     }
+
+     return NULL;
+ }
+
+ /*
   * Given the result tuple descriptor for a function with OUT parameters,
   * replace any polymorphic columns (ANYELEMENT etc) with correct data types
   * deduced from the input arguments. Returns TRUE if able to deduce all types,
diff --git a/src/include/funcapi.h b/src/include/funcapi.h
index 951af2a..0909961 100644
*** a/src/include/funcapi.h
--- b/src/include/funcapi.h
*************** typedef struct FuncCallContext
*** 143,148 ****
--- 143,151 ----
   *        get_call_result_type.  Note: the cases in which rowtypes cannot be
   *        determined are different from the cases for get_call_result_type.
   *        Do *not* use this if you can use one of the others.
+  *
+  * A related function, which should generally *not* be used for identifying
+  * a function's result type, is get_expr_result_tupdesc().
   *----------
   */

*************** extern TypeFuncClass get_func_result_typ
*** 165,170 ****
--- 168,175 ----
                       Oid *resultTypeId,
                       TupleDesc *resultTupleDesc);

+ extern TupleDesc get_expr_result_tupdesc(Node *expr, bool noError);
+
  extern bool resolve_polymorphic_argtypes(int numargs, Oid *argtypes,
                               char *argmodes,
                               Node *call_expr);
diff --git a/src/include/parser/parse_type.h b/src/include/parser/parse_type.h
index 7b843d0..9deec0d 100644
*** a/src/include/parser/parse_type.h
--- b/src/include/parser/parse_type.h
*************** extern Oid    typeTypeCollation(Type typ);
*** 46,55 ****
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! #define ISCOMPLEX(typeid) (typeidTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
--- 46,56 ----
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);
+ extern Oid    typeOrDomainTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! #define ISCOMPLEX(typeid) (typeOrDomainTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 5fe999e..21075f5 100644
*** a/src/test/regress/expected/domain.out
--- b/src/test/regress/expected/domain.out
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 198,203 ****
--- 198,291 ----
  (1 row)

  drop domain dia;
+ -- Test domains over composites
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ ERROR:  duplicate key value violates unique constraint "dcomptable_d1_key"
+ DETAIL:  Key (d1)=((1,2)) already exists.
+ insert into dcomptable (d1.r) values(11);
+ select * from dcomptable;
+   d1
+ -------
+  (1,2)
+  (3,4)
+  (11,)
+ (3 rows)
+
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+  r  | i | r  | i
+ ----+---+----+---
+   1 | 2 |  1 | 2
+   3 | 4 |  3 | 4
+  11 |   | 11 |
+ (3 rows)
+
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+   d1
+ -------
+  (11,)
+  (2,2)
+  (4,4)
+ (3 rows)
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+ ERROR:  column "d1" of table "dcomptable" contains values that violate the new constraint
+ select row(2,1)::dcomptype;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+     d1
+ ----------
+  (11,)
+  (99,)
+  (1,3)
+  (3,5)
+  (0,3)
+  (98,101)
+ (6 rows)
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+                                           QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+  Update on public.dcomptable
+    ->  Seq Scan on public.dcomptable
+          Output: ROW(((d1).r - '1'::double precision), ((d1).i + '1'::double precision)), ctid
+          Filter: ((dcomptable.d1).i > '0'::double precision)
+ (4 rows)
+
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+                                   Table "public.dcomptable"
+  Column |   Type    | Collation | Nullable | Default | Storage  | Stats target | Description
+ --------+-----------+-----------+----------+---------+----------+--------------+-------------
+  d1     | dcomptype |           |          |         | extended |              |
+ Indexes:
+     "dcomptable_d1_key" UNIQUE CONSTRAINT, btree (d1)
+ Rules:
+     silly AS
+     ON DELETE TO dcomptable DO INSTEAD  UPDATE dcomptable SET d1.r = (dcomptable.d1).r - 1::double precision, d1.i =
(dcomptable.d1).i+ 1::double precision 
+   WHERE (dcomptable.d1).i > 0::double precision
+
+ drop table dcomptable;
+ drop type comptype cascade;
+ NOTICE:  drop cascades to type dcomptype
  -- Test domains over arrays of composite
  create type comptype as (r float8, i float8);
  create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 5ec128d..ba45165 100644
*** a/src/test/regress/sql/domain.sql
--- b/src/test/regress/sql/domain.sql
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 120,125 ****
--- 120,164 ----
  drop domain dia;


+ -- Test domains over composites
+
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ insert into dcomptable (d1.r) values(11);
+
+ select * from dcomptable;
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+
+ select row(2,1)::dcomptype;  -- fail
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+
+ drop table dcomptable;
+ drop type comptype cascade;
+
+
  -- Test domains over arrays of composite

  create type comptype as (r float8, i float8);

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
"David G. Johnston"
Date:
On Thursday, July 13, 2017, Tom Lane <tgl@sss.pgh.pa.us> wrote:
regression=# select * from fdc();
  fdc
-------
 (1,2)
(1 row)


Select (fdc).* from fdc();  is considerably more intuitive that the cast.  Does that give the expected multi-column result?

David J. 

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
"David G. Johnston" <david.g.johnston@gmail.com> writes:
> On Thursday, July 13, 2017, Tom Lane <tgl@sss.pgh.pa.us> wrote:
>> regression=# select * from fdc();
>> fdc
>> -------
>> (1,2)
>> (1 row)

> Select (fdc).* from fdc();  is considerably more intuitive that the cast.
> Does that give the expected multi-column result?

Yeah, it does, although I'm not sure how intuitive it is that the
parentheses are significant ...

regression=#  select * from fdc(); fdc  
-------(1,2)
(1 row)

regression=# select fdc from fdc(); fdc  
-------(1,2)
(1 row)

regression=# select fdc.* from fdc(); fdc  
-------(1,2)
(1 row)

regression=# select (fdc).* from fdc();r | i 
---+---1 | 2
(1 row)
        regards, tom lane



Re: [HACKERS] Domains and arrays and composites, oh my

From
Robert Haas
Date:
On Thu, Jul 13, 2017 at 3:42 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
> Yeah, it does, although I'm not sure how intuitive it is that the
> parentheses are significant ...
>
> regression=# select fdc.* from fdc();
>   fdc
> -------
>  (1,2)
> (1 row)
>
> regression=# select (fdc).* from fdc();
>  r | i
> ---+---
>  1 | 2
> (1 row)

Not intuitive at all.

-- 
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company



Re: [HACKERS] Domains and arrays and composites, oh my

From
Andrew Dunstan
Date:

On 07/13/2017 03:19 PM, Tom Lane wrote:
> I wrote:
>> I started to look into allowing domains over composite types, which is
>> another never-implemented case that there's no very good reason not to
>> allow.  Well, other than the argument that the SQL standard only allows
>> domains over "predefined" (built-in) types ... but we blew past that
>> restriction ages ago.
> Attached is a draft patch that allows domains over composite types.
> I think it's probably complete on its own terms, but there are some
> questions around behavior of functions returning domain-over-composite
> that could use discussion, and some of the PLs need some follow-on work.
>
> The core principle here was to not modify execution-time behavior by
> adding domain checks to existing code paths.  Rather, I wanted the
> parser to insert CoerceToDomain nodes wherever checks would be needed.
> Row-returning node types such as RowExpr and FieldStore only return
> regular composite types, as before; to generate a value of a composite
> domain, we construct a value of the base type and then CoerceToDomain.
> (This is pretty analogous to what happens for domains over arrays.)
> Whole-row Vars can only have regular composite types as vartype,
> TupleDescs can only have regular composite types as tdtypeid, composite
> Datums only have regular composite type OIDs in datum_typeid.  (The last
> would be expected anyway, since the physical representation of a domain
> value is never different from that of its base type.)
>
> Doing it that way led to a nicely small patch, only about 160 net new
> lines of code.  However, not touching the executor meant not touching
> the behavior of functions that return domains, even if the domain is
> domain-over-composite.  In code terms this means that 
> get_call_result_type() and sibling functions will return TYPEFUNC_SCALAR
> not TYPEFUNC_COMPOSITE for such a result type.  The difficulty with
> changing that is that if these functions look through the domain, then
> the calling code (in, usually, a PL) will simply build and return a result
> of the underlying composite type, failing to apply any domain constraints.
> Trying to get out-of-core PLs on board with a change in those requirements
> seems like a risky proposition.
>
> Concretely, consider
>
> create type complex as (r float8, i float8);
> create domain dcomplex as complex;
>
> You can make a SQL-language function to return complex in either of two
> ways:
>
> create function fc() returns complex language sql
> as 'select 1.0::float8, 2.0::float8';
>
> create function fc() returns complex language sql
> as 'select row(1.0::float8, 2.0::float8)::complex';
>
> As the patch stands, though, only the second way works for domains over
> composite:
>
> regression=# create function fdc() returns dcomplex language sql
> as 'select 1.0::float8, 2.0::float8';
> ERROR:  return type mismatch in function declared to return dcomplex
> DETAIL:  Final statement must return exactly one column.
> CONTEXT:  SQL function "fdc"
> regression=# create function fdc() returns dcomplex language sql
> as 'select row(1.0::float8, 2.0)::dcomplex';
> CREATE FUNCTION
>
> Now, maybe that's fine.  SQL-language functions have never been very
> willing to insert implicit casts to get to the function result type,
> and certainly the only way that the first definition could be legal
> is if there were an implicit up-cast to the domain type.  It might be
> OK to just leave it like this, though some documentation about it
> would be a good idea.
>
> plpgsql functions seem generally okay as far as composite domain return
> types go, because they don't have anything corresponding to the row
> return convention of SQL functions.  And plpgsql's greater willingness
> to do implicit coercions reduces the notational burden, too.  But
> there's some work yet to be done to get plpgsql to realize that
> composite domain local variables have substructure.  For example,
> this works:
>
>     declare x complex;
>     ...
>     x.r := 1;
>
> but it fails if x is dcomplex.  But ISTM that that would be better
> handled as a followon feature patch.  I suspect that the other PLs may
> have similar issues where it'd be nice to allow domain-over-composite
> to act like a plain composite for specific purposes; but I've not looked.
>
> Another issue related to function result types is that the parser
> considers a function-in-FROM returning a composite domain to be
> producing a scalar result not a rowtype.  Thus you get this for a
> function returning complex:
>
> regression=# select * from fc();
>  r | i 
> ---+---
>  1 | 2
> (1 row)
>
> but this for a function returning dcomplex:
>
> regression=# select * from fdc();
>   fdc  
> -------
>  (1,2)
> (1 row)
>
> I think that that could be changed with only local changes in parse
> analysis, but do we want to change it?  Arguably, making fdc() act the
> same as fc() here would amount to implicitly downcasting the domain to
> its base type.  But doing so here is optional, not necessary in order to
> make the statement sane at all, and it's arguable that we shouldn't do
> that if the user didn't tell us to.  A user who does want that to happen
> can downcast explicitly:
>
> regression=# select * from cast(fdc() as complex);
>  r | i 
> ---+---
>  1 | 2
> (1 row)
>
> (For arcane syntactic reasons you can't abbreviate CAST with :: here.)
> Another point is that if you do want the domain value as a domain
> value, and not smashed to its base type, it would be hard to get at
> if the parser acts this way --- "foo.*" would end up producing the base
> rowtype, or if it didn't, we'd have some issues with the previously
> noted rule about whole-row Vars never having domain types.
>
> So there's a case to be made that this behavior is fine as-is, but
> certainly you could also argue that it's a POLA violation.
>
> Digression: one reason I'm hesitant to introduce inessential reductions
> of domains to base types is that I'm looking ahead to arrays over
> domains, which will provide a workaround for the people who complain
> that they wish 2-D arrays would work type-wise like arrays of 1-D array
> objects.  If you "create domain inta as int[]" then inta[] would act
> like an array of array objects, mostly solving the problem I think.
> But it solves the problem only because we don't consider that a domain
> is indistinguishable from its base type.  It's hard to be sure without
> having done the work yet, but I think there will be cases where being
> over-eager to treat a domain as its base type might break the behavior
> we want for that case.  So I don't want to create a precedent for that
> here.
>
> Thoughts?
>


This is a pretty nice patch, and very small indeed all things
considered. From a code point of view I have no criticism, although
maybe we need to be a bit more emphatic in the header file comments
about the unwisdom of using get_expr_result_tupdesc().

I do think that treating a function returning a domain-over-composite
differently from one returning a base composite is a POLA. We'd be very
hard put to explain the reasons for it to an end user.

I also think we shouldn't commit this until we have accompanying patches
for the core PLs, at least for plpgsql but I bet there are things that
should be fixed for the others too.


cheers

andrew

-- 
Andrew Dunstan                https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services



-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
Andrew Dunstan <andrew.dunstan@2ndquadrant.com> writes:
> On 07/13/2017 03:19 PM, Tom Lane wrote:
>> Attached is a draft patch that allows domains over composite types.
>> I think it's probably complete on its own terms, but there are some
>> questions around behavior of functions returning domain-over-composite
>> that could use discussion, and some of the PLs need some follow-on work.

> This is a pretty nice patch, and very small indeed all things
> considered. From a code point of view I have no criticism, although
> maybe we need to be a bit more emphatic in the header file comments
> about the unwisdom of using get_expr_result_tupdesc().

Thanks for reviewing!

> I do think that treating a function returning a domain-over-composite
> differently from one returning a base composite is a POLA. We'd be very
> hard put to explain the reasons for it to an end user.

Do you have any thoughts about how we ought to resolve that?

> I also think we shouldn't commit this until we have accompanying patches
> for the core PLs, at least for plpgsql but I bet there are things that
> should be fixed for the others too.

For my own part, I think it would be reasonable to commit the core patch
once we've resolved the question of what to do with the case of
function-in-FROM returning domain over composite.  That's core parser
behavior so it should be part of the same patch.  I think addressing
each PL separately in followon patches would be fine and would help to
avoid the giant-unreviewable-patch syndrome.  It is important to get
all the related work done in one release cycle, but since we're just
starting v11 I'm not too worried about that.
        regards, tom lane


-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Andrew Dunstan
Date:

On 09/28/2017 01:02 PM, Tom Lane wrote:
>
>> I do think that treating a function returning a domain-over-composite
>> differently from one returning a base composite is a POLA. We'd be very
>> hard put to explain the reasons for it to an end user.
> Do you have any thoughts about how we ought to resolve that?
>
>


Not offhand. Maybe we need to revisit the decision not to modify the
executor at all. Obviously that would make the patch a good deal more
invasive ;-(  One thought I had was that we could invent a new return
type of TYPEFUNC_DOMAIN_COMPOSITE so there would be less danger of a PL
just treating it as an unconstrained base type as it might do if it saw
TYPEFUNC_COMPOSITE.

Maybe I'm wrong, but I have a strong suspicion that of we leave it like
this now we'll regret it in the future.

cheers

andrew

-- 
Andrew Dunstan                https://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services



-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
Andrew Dunstan <andrew.dunstan@2ndquadrant.com> writes:
> On 09/28/2017 01:02 PM, Tom Lane wrote:
>>> I do think that treating a function returning a domain-over-composite
>>> differently from one returning a base composite is a POLA. We'd be very
>>> hard put to explain the reasons for it to an end user.

>> Do you have any thoughts about how we ought to resolve that?

> Not offhand. Maybe we need to revisit the decision not to modify the
> executor at all.

I think it's more of a parse analysis change: the issue is whether to
smash a function's result type to base when determining whether it emits
columns.  Maybe we could just do that in that context, and otherwise leave
domains alone.

> One thought I had was that we could invent a new return
> type of TYPEFUNC_DOMAIN_COMPOSITE so there would be less danger of a PL
> just treating it as an unconstrained base type as it might do if it saw
> TYPEFUNC_COMPOSITE.

Hmm.  That would be a way of forcing the issue, no doubt ...
        regards, tom lane


-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
I wrote:
> Andrew Dunstan <andrew.dunstan@2ndquadrant.com> writes:
>> On 09/28/2017 01:02 PM, Tom Lane wrote:
>>>> I do think that treating a function returning a domain-over-composite
>>>> differently from one returning a base composite is a POLA. We'd be very
>>>> hard put to explain the reasons for it to an end user.

>>> Do you have any thoughts about how we ought to resolve that?

>> Not offhand. Maybe we need to revisit the decision not to modify the
>> executor at all.

> I think it's more of a parse analysis change: the issue is whether to
> smash a function's result type to base when determining whether it emits
> columns.  Maybe we could just do that in that context, and otherwise leave
> domains alone.

After fooling with that for awhile, I concluded that the only reasonable
path forward is to go ahead and modify the behavior of
get_expr_result_type and sibling routines.  While this fixes the parser
behavior to be pretty much what I think we want, it means that we've got
holes to fill in a lot of other places.  Most of them will manifest as
unexpected "domaintypename is not a composite type" errors, but there
are definitely places where the net effect is to silently fail to enforce
domain constraints against a constructed row value :-(.  In the attached
still-WIP patch, I think that I've got most of the core code fixed, but
there are at least these holes remaining to fill:

* json_populate_record and sibling routines won't enforce domain
constraints; depending on how they're called, you might or might not
get a "not a composite type" error.  This is because they use two
different methods for getting the target type OID depending on whether
the input prototype record is NULL.  Maybe that was a bad idea.
(I'm disinclined to try to fix this code right now since there are
pending bug fixes nearby; better to wait till that dust settles.)

* Ditto for hstore's populate_record, which is pretty much same logic.

* plpgsql mostly seems to work, but not quite 100%: RETURN QUERY will
fail to enforce domain constraints if the return type is domain over
composite.  It also still needs feature extension to handle d-over-c
variables more fully (e.g. allow field assignment).

* I haven't looked at the other PLs much; I believe they will mostly
fail safe with "not a composite type" errors, but I wouldn't swear
that all code paths will.

It seems like this is probably the way forward, but I'm slightly
discouraged by the fact that the patch footprint is getting bigger
and there are paths where we can get domain-enforcement omissions
rather than something more benign.  Still, we had lots of
domain-enforcement omissions in the early days of the existing
domain feature, if memory serves.  Maybe we should just accept
that working through that will be a process.

>> One thought I had was that we could invent a new return
>> type of TYPEFUNC_DOMAIN_COMPOSITE so there would be less danger of a PL
>> just treating it as an unconstrained base type as it might do if it saw
>> TYPEFUNC_COMPOSITE.

> Hmm.  That would be a way of forcing the issue, no doubt ...

I did that, but it turns out not to help much; turns out a lot of the
broken code is doing stuff on the basis of type_is_rowtype(), which
this patch allows to return true for domains over composite.  Maybe
we should undo that and invent a separate type_is_rowtype_or_domain()
function to be used only by repaired code, but that seems pretty ugly :-(

Anyway, PFA an updated patch that also fixes some conflicts with the
already-committed arrays-of-domains patch.

            regards, tom lane

diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 245a374..1bd8a58 100644
*** a/src/backend/catalog/pg_inherits.c
--- b/src/backend/catalog/pg_inherits.c
*************** has_superclass(Oid relationId)
*** 301,306 ****
--- 301,311 ----
  /*
   * Given two type OIDs, determine whether the first is a complex type
   * (class type) that inherits from the second.
+  *
+  * This essentially asks whether the first type is guaranteed to be coercible
+  * to the second.  Therefore, we allow the first type to be a domain over a
+  * complex type that inherits from the second; that creates no difficulties.
+  * But the second type cannot be a domain.
   */
  bool
  typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId)
*************** typeInheritsFrom(Oid subclassTypeId, Oid
*** 314,322 ****
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeidTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
--- 319,327 ----
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeOrDomainTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type or domain over one */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 571856e..47916cf 100644
*** a/src/backend/catalog/pg_proc.c
--- b/src/backend/catalog/pg_proc.c
*************** ProcedureCreate(const char *procedureNam
*** 262,268 ****
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeidTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
--- 262,268 ----
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeOrDomainTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index c1b87e0..7df942b 100644
*** a/src/backend/commands/typecmds.c
--- b/src/backend/commands/typecmds.c
*************** DefineDomain(CreateDomainStmt *stmt)
*** 798,810 ****
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, another domain, an enum or a range
!      * type. Domains over pseudotypes would create a security hole.  Domains
!      * over composite types might be made to work in the future, but not
!      * today.
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
--- 798,813 ----
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, a composite type, another domain,
!      * an enum or a range type.  Domains over pseudotypes would create a
!      * security hole.  (It would be shorter to code this to just check for
!      * pseudotypes; but it seems safer to call out the specific typtypes that
!      * are supported, rather than assume that all future typtypes would be
!      * automatically supported.)
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
+         typtype != TYPTYPE_COMPOSITE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index c5e97ef..a0f537b 100644
*** a/src/backend/executor/execExprInterp.c
--- b/src/backend/executor/execExprInterp.c
*************** ExecEvalWholeRowVar(ExprState *state, Ex
*** 3469,3476 ****
               * generates an INT4 NULL regardless of the dropped column type).
               * If we find a dropped column and cannot verify that case (1)
               * holds, we have to use the slow path to check (2) for each row.
               */
!             var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1);

              slot_tupdesc = slot->tts_tupleDescriptor;

--- 3469,3480 ----
               * generates an INT4 NULL regardless of the dropped column type).
               * If we find a dropped column and cannot verify that case (1)
               * holds, we have to use the slow path to check (2) for each row.
+              *
+              * If vartype is a domain over composite, just look through that
+              * to the base composite type.
               */
!             var_tupdesc = lookup_rowtype_tupdesc_domain(variable->vartype,
!                                                         -1, false);

              slot_tupdesc = slot->tts_tupleDescriptor;

diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c
index cce771d..aabe26c 100644
*** a/src/backend/executor/execSRF.c
--- b/src/backend/executor/execSRF.c
*************** init_sexpr(Oid foid, Oid input_collation
*** 734,740 ****
          /* Must save tupdesc in sexpr's context */
          oldcontext = MemoryContextSwitchTo(sexprCxt);

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 734,741 ----
          /* Must save tupdesc in sexpr's context */
          oldcontext = MemoryContextSwitchTo(sexprCxt);

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 42a4ca9..e734bd9 100644
*** a/src/backend/executor/functions.c
--- b/src/backend/executor/functions.c
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1711,1717 ****
              }
          }

!         /* Is the rowtype fixed, or determined only at runtime? */
          if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          {
              /*
--- 1711,1725 ----
              }
          }

!         /*
!          * Is the rowtype fixed, or determined only at runtime?
!          *
!          * Note: you might expect that TYPEFUNC_COMPOSITE_DOMAIN should be
!          * treated like TYPEFUNC_COMPOSITE, but we intentionally don't.  This
!          * is because SQL functions don't provide any implicit casting to the
!          * result type, so there is no way to produce a domain-over-composite
!          * result except by computing it as an explicit single-column result.
!          */
          if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          {
              /*
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 9f87a7e..de476ac 100644
*** a/src/backend/executor/nodeFunctionscan.c
--- b/src/backend/executor/nodeFunctionscan.c
*************** ExecInitFunctionScan(FunctionScan *node,
*** 383,389 ****
                                              &funcrettype,
                                              &tupdesc);

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 383,390 ----
                                              &funcrettype,
                                              &tupdesc);

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index b58eb0f..7a67653 100644
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
*************** makeVarFromTargetEntry(Index varno,
*** 120,127 ****
   * table entry, and varattno == 0 to signal that it references the whole
   * tuple.  (Use of zero here is unclean, since it could easily be confused
   * with error cases, but it's not worth changing now.)  The vartype indicates
!  * a rowtype; either a named composite type, or RECORD.  This function
!  * encapsulates the logic for determining the correct rowtype OID to use.
   *
   * If allowScalar is true, then for the case where the RTE is a single function
   * returning a non-composite result type, we produce a normal Var referencing
--- 120,129 ----
   * table entry, and varattno == 0 to signal that it references the whole
   * tuple.  (Use of zero here is unclean, since it could easily be confused
   * with error cases, but it's not worth changing now.)  The vartype indicates
!  * a rowtype; either a named composite type, or a domain over a named
!  * composite type (only possible if the RTE is a function returning that),
!  * or RECORD.  This function encapsulates the logic for determining the
!  * correct rowtype OID to use.
   *
   * If allowScalar is true, then for the case where the RTE is a single function
   * returning a non-composite result type, we produce a normal Var referencing
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 7961362..1a82749 100644
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
*************** CommuteRowCompareExpr(RowCompareExpr *cl
*** 2356,2361 ****
--- 2356,2365 ----
   * is still what it was when the expression was parsed.  This is needed to
   * guard against improper simplification after ALTER COLUMN TYPE.  (XXX we
   * may well need to make similar checks elsewhere?)
+  *
+  * rowtypeid may come from a whole-row Var, and therefore it can be a domain
+  * over composite, but for this purpose we only care about checking the type
+  * of a contained field.
   */
  static bool
  rowtype_field_matches(Oid rowtypeid, int fieldnum,
*************** rowtype_field_matches(Oid rowtypeid, int
*** 2368,2374 ****
      /* No issue for RECORD, since there is no way to ALTER such a type */
      if (rowtypeid == RECORDOID)
          return true;
!     tupdesc = lookup_rowtype_tupdesc(rowtypeid, -1);
      if (fieldnum <= 0 || fieldnum > tupdesc->natts)
      {
          ReleaseTupleDesc(tupdesc);
--- 2372,2378 ----
      /* No issue for RECORD, since there is no way to ALTER such a type */
      if (rowtypeid == RECORDOID)
          return true;
!     tupdesc = lookup_rowtype_tupdesc_domain(rowtypeid, -1, false);
      if (fieldnum <= 0 || fieldnum > tupdesc->natts)
      {
          ReleaseTupleDesc(tupdesc);
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 53457dc..def41b3 100644
*** a/src/backend/parser/parse_coerce.c
--- b/src/backend/parser/parse_coerce.c
*************** coerce_type(ParseState *pstate, Node *no
*** 499,507 ****
--- 499,524 ----
           * Input class type is a subclass of target, so generate an
           * appropriate runtime conversion (removing unneeded columns and
           * possibly rearranging the ones that are wanted).
+          *
+          * We will also get here when the input is a domain over a subclass of
+          * the target type.  To keep life simple for the executor, we define
+          * ConvertRowtypeExpr as only working between regular composite types;
+          * therefore, in such cases insert a RelabelType to smash the input
+          * expression down to its base type.
           */
+         Oid            baseTypeId = getBaseType(inputTypeId);
          ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);

+         if (baseTypeId != inputTypeId)
+         {
+             RelabelType *rt = makeRelabelType((Expr *) node,
+                                               baseTypeId, -1,
+                                               InvalidOid,
+                                               COERCE_IMPLICIT_CAST);
+
+             rt->location = location;
+             node = (Node *) rt;
+         }
          r->arg = (Expr *) node;
          r->resulttype = targetTypeId;
          r->convertformat = cformat;
*************** coerce_record_to_complex(ParseState *pst
*** 966,971 ****
--- 983,990 ----
                           int location)
  {
      RowExpr    *rowexpr;
+     Oid            baseTypeId;
+     int32        baseTypeMod = -1;
      TupleDesc    tupdesc;
      List       *args = NIL;
      List       *newargs;
*************** coerce_record_to_complex(ParseState *pst
*** 1001,1007 ****
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     tupdesc = lookup_rowtype_tupdesc(targetTypeId, -1);
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
--- 1020,1033 ----
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     /*
!      * Look up the composite type, accounting for possibility that what we are
!      * given is a domain over composite.
!      */
!     baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!     tupdesc = lookup_rowtype_tupdesc(baseTypeId, baseTypeMod);
!
!     /* Process the fields */
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
*************** coerce_record_to_complex(ParseState *pst
*** 1070,1079 ****

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = targetTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
      return (Node *) rowexpr;
  }

--- 1096,1117 ----

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = baseTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
+
+     /* If target is a domain, apply constraints */
+     if (baseTypeId != targetTypeId)
+     {
+         rowexpr->row_format = COERCE_IMPLICIT_CAST;
+         return coerce_to_domain((Node *) rowexpr,
+                                 baseTypeId, baseTypeMod,
+                                 targetTypeId,
+                                 ccontext, cformat, location,
+                                 false);
+     }
+
      return (Node *) rowexpr;
  }

*************** is_complex_array(Oid typid)
*** 2401,2413 ****

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId.  (This is conceptually similar to the subtype
!  * relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeidTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
--- 2439,2451 ----

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId, or is a domain over such a row type.  (This is conceptually
!  * similar to the subtype relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeOrDomainTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 2f2f2c7..fc0d6bc 100644
*** a/src/backend/parser/parse_func.c
--- b/src/backend/parser/parse_func.c
*************** ParseComplexProjection(ParseState *pstat
*** 1819,1836 ****
      }

      /*
!      * Else do it the hard way with get_expr_result_type().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else if (get_expr_result_type(first_arg, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          return NULL;            /* unresolvable RECORD type */
-     Assert(tupdesc);

      for (i = 0; i < tupdesc->natts; i++)
      {
--- 1819,1837 ----
      }

      /*
!      * Else do it the hard way with get_expr_result_tupdesc().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else
!         tupdesc = get_expr_result_tupdesc(first_arg, true);
!     if (!tupdesc)
          return NULL;            /* unresolvable RECORD type */

      for (i = 0; i < tupdesc->natts; i++)
      {
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index a9273af..ca32a37 100644
*** a/src/backend/parser/parse_relation.c
--- b/src/backend/parser/parse_relation.c
*************** addRangeTableEntryForFunction(ParseState
*** 1496,1502 ****
                           parser_errposition(pstate, exprLocation(funcexpr))));
          }

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 1496,1503 ----
                           parser_errposition(pstate, exprLocation(funcexpr))));
          }

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
*************** expandRTE(RangeTblEntry *rte, int rtinde
*** 2245,2251 ****
                      functypclass = get_expr_result_type(rtfunc->funcexpr,
                                                          &funcrettype,
                                                          &tupdesc);
!                     if (functypclass == TYPEFUNC_COMPOSITE)
                      {
                          /* Composite data type, e.g. a table's row type */
                          Assert(tupdesc);
--- 2246,2253 ----
                      functypclass = get_expr_result_type(rtfunc->funcexpr,
                                                          &funcrettype,
                                                          &tupdesc);
!                     if (functypclass == TYPEFUNC_COMPOSITE ||
!                         functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
                      {
                          /* Composite data type, e.g. a table's row type */
                          Assert(tupdesc);
*************** get_rte_attribute_type(RangeTblEntry *rt
*** 2765,2771 ****
                                                              &funcrettype,
                                                              &tupdesc);

!                         if (functypclass == TYPEFUNC_COMPOSITE)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
--- 2767,2774 ----
                                                              &funcrettype,
                                                              &tupdesc);

!                         if (functypclass == TYPEFUNC_COMPOSITE ||
!                             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
*************** get_rte_attribute_is_dropped(RangeTblEnt
*** 2966,2979 ****
                      if (attnum > atts_done &&
                          attnum <= atts_done + rtfunc->funccolcount)
                      {
-                         TypeFuncClass functypclass;
-                         Oid            funcrettype;
                          TupleDesc    tupdesc;

!                         functypclass = get_expr_result_type(rtfunc->funcexpr,
!                                                             &funcrettype,
!                                                             &tupdesc);
!                         if (functypclass == TYPEFUNC_COMPOSITE)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
--- 2969,2979 ----
                      if (attnum > atts_done &&
                          attnum <= atts_done + rtfunc->funccolcount)
                      {
                          TupleDesc    tupdesc;

!                         tupdesc = get_expr_result_tupdesc(rtfunc->funcexpr,
!                                                           true);
!                         if (tupdesc)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 2547524..16d3dba 100644
*** a/src/backend/parser/parse_target.c
--- b/src/backend/parser/parse_target.c
*************** transformAssignmentIndirection(ParseStat
*** 725,730 ****
--- 725,732 ----
          else
          {
              FieldStore *fstore;
+             Oid            baseTypeId;
+             int32        baseTypeMod;
              Oid            typrelid;
              AttrNumber    attnum;
              Oid            fieldTypeId;
*************** transformAssignmentIndirection(ParseStat
*** 752,758 ****

              /* No subscripts, so can process field selection here */

!             typrelid = typeidTypeRelid(targetTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
--- 754,767 ----

              /* No subscripts, so can process field selection here */

!             /*
!              * Look up the composite type, accounting for possibility that
!              * what we are given is a domain over composite.
!              */
!             baseTypeMod = targetTypMod;
!             baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!
!             typrelid = typeidTypeRelid(baseTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
*************** transformAssignmentIndirection(ParseStat
*** 796,802 ****
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = targetTypeId;

              return (Node *) fstore;
          }
--- 805,821 ----
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = baseTypeId;
!
!             /* If target is a domain, apply constraints */
!             if (baseTypeId != targetTypeId)
!                 return coerce_to_domain((Node *) fstore,
!                                         baseTypeId, baseTypeMod,
!                                         targetTypeId,
!                                         COERCION_IMPLICIT,
!                                         COERCE_IMPLICIT_CAST,
!                                         location,
!                                         false);

              return (Node *) fstore;
          }
*************** ExpandRowReference(ParseState *pstate, N
*** 1387,1408 ****
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.  We use
!      * get_expr_result_type() because that can handle references to functions
!      * returning anonymous record types.  If that fails, use
!      * lookup_rowtype_tupdesc(), which will almost certainly fail as well, but
!      * it will give an appropriate error message.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
--- 1406,1423 ----
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.
!      * get_expr_result_tupdesc() handles this conveniently.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else
!         tupleDesc = get_expr_result_tupdesc(expr, false);
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
*************** expandRecordVariable(ParseState *pstate,
*** 1610,1624 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!
!     return tupleDesc;
  }


--- 1625,1633 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     return get_expr_result_tupdesc(expr, false);
  }


diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index d0b3fbe..b032651 100644
*** a/src/backend/parser/parse_type.c
--- b/src/backend/parser/parse_type.c
*************** stringTypeDatum(Type tp, char *string, i
*** 641,647 ****
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /* given a typeid, return the type's typrelid (associated relation, if any) */
  Oid
  typeidTypeRelid(Oid type_id)
  {
--- 641,650 ----
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /*
!  * Given a typeid, return the type's typrelid (associated relation), if any.
!  * Returns InvalidOid if type is not a composite type.
!  */
  Oid
  typeidTypeRelid(Oid type_id)
  {
*************** typeidTypeRelid(Oid type_id)
*** 652,658 ****
      typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
      if (!HeapTupleIsValid(typeTuple))
          elog(ERROR, "cache lookup failed for type %u", type_id);
-
      type = (Form_pg_type) GETSTRUCT(typeTuple);
      result = type->typrelid;
      ReleaseSysCache(typeTuple);
--- 655,660 ----
*************** typeidTypeRelid(Oid type_id)
*** 660,665 ****
--- 662,699 ----
  }

  /*
+  * Given a typeid, return the type's typrelid (associated relation), if any.
+  * Returns InvalidOid if type is not a composite type or a domain over one.
+  * This is the same as typeidTypeRelid(getBaseType(type_id)), but faster.
+  */
+ Oid
+ typeOrDomainTypeRelid(Oid type_id)
+ {
+     HeapTuple    typeTuple;
+     Form_pg_type type;
+     Oid            result;
+
+     for (;;)
+     {
+         typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
+         if (!HeapTupleIsValid(typeTuple))
+             elog(ERROR, "cache lookup failed for type %u", type_id);
+         type = (Form_pg_type) GETSTRUCT(typeTuple);
+         if (type->typtype != TYPTYPE_DOMAIN)
+         {
+             /* Not a domain, so done looking through domains */
+             break;
+         }
+         /* It is a domain, so examine the base type instead */
+         type_id = type->typbasetype;
+         ReleaseSysCache(typeTuple);
+     }
+     result = type->typrelid;
+     ReleaseSysCache(typeTuple);
+     return result;
+ }
+
+ /*
   * error context callback for parse failure during parseTypeString()
   */
  static void
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 84759b6..b1e70a0 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** get_name_for_var_field(Var *var, int fie
*** 6731,6747 ****

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_type. If that fails, we try
!      * lookup_rowtype_tupdesc, which will probably fail too, but will ereport
!      * an acceptable message.
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         if (get_expr_result_type((Node *) var, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!             tupleDesc = lookup_rowtype_tupdesc_copy(exprType((Node *) var),
!                                                     exprTypmod((Node *) var));
!         Assert(tupleDesc);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
--- 6731,6742 ----

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_tupdesc().
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         tupleDesc = get_expr_result_tupdesc((Node *) var, false);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
*************** get_name_for_var_field(Var *var, int fie
*** 7044,7057 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!     Assert(tupleDesc);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
--- 7039,7047 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     tupleDesc = get_expr_result_tupdesc(expr, false);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index b7a14dc..48961e3 100644
*** a/src/backend/utils/cache/lsyscache.c
--- b/src/backend/utils/cache/lsyscache.c
*************** get_typtype(Oid typid)
*** 2398,2409 ****
   * type_is_rowtype
   *
   *        Convenience function to determine whether a type OID represents
!  *        a "rowtype" type --- either RECORD or a named composite type.
   */
  bool
  type_is_rowtype(Oid typid)
  {
!     return (typid == RECORDOID || get_typtype(typid) == TYPTYPE_COMPOSITE);
  }

  /*
--- 2398,2423 ----
   * type_is_rowtype
   *
   *        Convenience function to determine whether a type OID represents
!  *        a "rowtype" type --- either RECORD or a named composite type
!  *        (including a domain over a named composite type).
   */
  bool
  type_is_rowtype(Oid typid)
  {
!     if (typid == RECORDOID)
!         return true;            /* easy case */
!     switch (get_typtype(typid))
!     {
!         case TYPTYPE_COMPOSITE:
!             return true;
!         case TYPTYPE_DOMAIN:
!             if (get_typtype(getBaseType(typid)) == TYPTYPE_COMPOSITE)
!                 return true;
!             break;
!         default:
!             break;
!     }
!     return false;
  }

  /*
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 16c52c5..197b9e5 100644
*** a/src/backend/utils/cache/typcache.c
--- b/src/backend/utils/cache/typcache.c
*************** lookup_type_cache(Oid type_id, int flags
*** 717,723 ****
      /*
       * If requested, get information about a domain type
       */
!     if ((flags & TYPECACHE_DOMAIN_INFO) &&
          (typentry->flags & TCFLAGS_CHECKED_DOMAIN_CONSTRAINTS) == 0 &&
          typentry->typtype == TYPTYPE_DOMAIN)
      {
--- 717,731 ----
      /*
       * If requested, get information about a domain type
       */
!     if ((flags & TYPECACHE_DOMAIN_BASE_INFO) &&
!         typentry->typtype == TYPTYPE_DOMAIN &&
!         typentry->domainBaseType == InvalidOid)
!     {
!         typentry->domainBaseTypmod = -1;
!         typentry->domainBaseType =
!             getBaseTypeAndTypmod(type_id, &typentry->domainBaseTypmod);
!     }
!     if ((flags & TYPECACHE_DOMAIN_CONSTR_INFO) &&
          (typentry->flags & TCFLAGS_CHECKED_DOMAIN_CONSTRAINTS) == 0 &&
          typentry->typtype == TYPTYPE_DOMAIN)
      {
*************** InitDomainConstraintRef(Oid type_id, Dom
*** 1136,1142 ****
                          MemoryContext refctx, bool need_exprstate)
  {
      /* Look up the typcache entry --- we assume it survives indefinitely */
!     ref->tcache = lookup_type_cache(type_id, TYPECACHE_DOMAIN_INFO);
      ref->need_exprstate = need_exprstate;
      /* For safety, establish the callback before acquiring a refcount */
      ref->refctx = refctx;
--- 1144,1150 ----
                          MemoryContext refctx, bool need_exprstate)
  {
      /* Look up the typcache entry --- we assume it survives indefinitely */
!     ref->tcache = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
      ref->need_exprstate = need_exprstate;
      /* For safety, establish the callback before acquiring a refcount */
      ref->refctx = refctx;
*************** DomainHasConstraints(Oid type_id)
*** 1227,1233 ****
       * Note: a side effect is to cause the typcache's domain data to become
       * valid.  This is fine since we'll likely need it soon if there is any.
       */
!     typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_INFO);

      return (typentry->domainData != NULL);
  }
--- 1235,1241 ----
       * Note: a side effect is to cause the typcache's domain data to become
       * valid.  This is fine since we'll likely need it soon if there is any.
       */
!     typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);

      return (typentry->domainData != NULL);
  }
*************** lookup_rowtype_tupdesc_copy(Oid type_id,
*** 1526,1531 ****
--- 1534,1586 ----
  }

  /*
+  * lookup_rowtype_tupdesc_domain
+  *
+  * Same as lookup_rowtype_tupdesc_noerror(), except that the type can also be
+  * a domain over a named composite type; so this is effectively equivalent to
+  * lookup_rowtype_tupdesc_noerror(getBaseType(type_id), typmod, noError)
+  * except for being a tad faster.
+  *
+  * Note: the reason we don't fold the look-through-domain behavior into plain
+  * lookup_rowtype_tupdesc() is that we want callers to know they might be
+  * dealing with a domain.  Otherwise they might construct a tuple that should
+  * be of the domain type, but not apply domain constraints.
+  */
+ TupleDesc
+ lookup_rowtype_tupdesc_domain(Oid type_id, int32 typmod, bool noError)
+ {
+     TupleDesc    tupDesc;
+
+     if (type_id != RECORDOID)
+     {
+         /*
+          * Check for domain or named composite type.  We might as well load
+          * whichever data is needed.
+          */
+         TypeCacheEntry *typentry;
+
+         typentry = lookup_type_cache(type_id,
+                                      TYPECACHE_TUPDESC |
+                                      TYPECACHE_DOMAIN_BASE_INFO);
+         if (typentry->typtype == TYPTYPE_DOMAIN)
+             return lookup_rowtype_tupdesc_noerror(typentry->domainBaseType,
+                                                   typentry->domainBaseTypmod,
+                                                   noError);
+         if (typentry->tupDesc == NULL && !noError)
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("type %s is not composite",
+                             format_type_be(type_id))));
+         tupDesc = typentry->tupDesc;
+     }
+     else
+         tupDesc = lookup_rowtype_tupdesc_internal(type_id, typmod, noError);
+     if (tupDesc != NULL)
+         PinTupleDesc(tupDesc);
+     return tupDesc;
+ }
+
+ /*
   * Hash function for the hash table of RecordCacheEntry.
   */
  static uint32
diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c
index 9c3f451..5caa876 100644
*** a/src/backend/utils/fmgr/funcapi.c
--- b/src/backend/utils/fmgr/funcapi.c
*************** static TypeFuncClass internal_get_result
*** 39,45 ****
  static bool resolve_polymorphic_tupdesc(TupleDesc tupdesc,
                              oidvector *declared_args,
                              Node *call_expr);
! static TypeFuncClass get_type_func_class(Oid typid);


  /*
--- 39,45 ----
  static bool resolve_polymorphic_tupdesc(TupleDesc tupdesc,
                              oidvector *declared_args,
                              Node *call_expr);
! static TypeFuncClass get_type_func_class(Oid typid, Oid *base_typeid);


  /*
*************** get_expr_result_type(Node *expr,
*** 246,259 ****
      {
          /* handle as a generic expression; no chance to resolve RECORD */
          Oid            typid = exprType(expr);

          if (resultTypeId)
              *resultTypeId = typid;
          if (resultTupleDesc)
              *resultTupleDesc = NULL;
!         result = get_type_func_class(typid);
!         if (result == TYPEFUNC_COMPOSITE && resultTupleDesc)
!             *resultTupleDesc = lookup_rowtype_tupdesc_copy(typid, -1);
      }

      return result;
--- 246,262 ----
      {
          /* handle as a generic expression; no chance to resolve RECORD */
          Oid            typid = exprType(expr);
+         Oid            base_typid;

          if (resultTypeId)
              *resultTypeId = typid;
          if (resultTupleDesc)
              *resultTupleDesc = NULL;
!         result = get_type_func_class(typid, &base_typid);
!         if ((result == TYPEFUNC_COMPOSITE ||
!              result == TYPEFUNC_COMPOSITE_DOMAIN) &&
!             resultTupleDesc)
!             *resultTupleDesc = lookup_rowtype_tupdesc_copy(base_typid, -1);
      }

      return result;
*************** internal_get_result_type(Oid funcid,
*** 296,301 ****
--- 299,305 ----
      HeapTuple    tp;
      Form_pg_proc procform;
      Oid            rettype;
+     Oid            base_rettype;
      TupleDesc    tupdesc;

      /* First fetch the function's pg_proc row to inspect its rettype */
*************** internal_get_result_type(Oid funcid,
*** 363,374 ****
          *resultTupleDesc = NULL;    /* default result */

      /* Classify the result type */
!     result = get_type_func_class(rettype);
      switch (result)
      {
          case TYPEFUNC_COMPOSITE:
              if (resultTupleDesc)
!                 *resultTupleDesc = lookup_rowtype_tupdesc_copy(rettype, -1);
              /* Named composite types can't have any polymorphic columns */
              break;
          case TYPEFUNC_SCALAR:
--- 367,379 ----
          *resultTupleDesc = NULL;    /* default result */

      /* Classify the result type */
!     result = get_type_func_class(rettype, &base_rettype);
      switch (result)
      {
          case TYPEFUNC_COMPOSITE:
+         case TYPEFUNC_COMPOSITE_DOMAIN:
              if (resultTupleDesc)
!                 *resultTupleDesc = lookup_rowtype_tupdesc_copy(base_rettype, -1);
              /* Named composite types can't have any polymorphic columns */
              break;
          case TYPEFUNC_SCALAR:
*************** internal_get_result_type(Oid funcid,
*** 394,399 ****
--- 399,444 ----
  }

  /*
+  * get_expr_result_tupdesc
+  *        Get a tupdesc describing the result of a composite-valued expression
+  *
+  * If expression is not composite or rowtype can't be determined, returns NULL
+  * if noError is true, else throws error.
+  *
+  * This is a simpler version of get_expr_result_type() for use when the caller
+  * is only interested in determinate rowtype results.
+  */
+ TupleDesc
+ get_expr_result_tupdesc(Node *expr, bool noError)
+ {
+     TupleDesc    tupleDesc;
+     TypeFuncClass functypclass;
+
+     functypclass = get_expr_result_type(expr, NULL, &tupleDesc);
+
+     if (functypclass == TYPEFUNC_COMPOSITE ||
+         functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
+         return tupleDesc;
+
+     if (!noError)
+     {
+         Oid            exprTypeId = exprType(expr);
+
+         if (exprTypeId != RECORDOID)
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("type %s is not composite",
+                             format_type_be(exprTypeId))));
+         else
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("record type has not been registered")));
+     }
+
+     return NULL;
+ }
+
+ /*
   * Given the result tuple descriptor for a function with OUT parameters,
   * replace any polymorphic columns (ANYELEMENT etc) with correct data types
   * deduced from the input arguments. Returns TRUE if able to deduce all types,
*************** resolve_polymorphic_argtypes(int numargs
*** 741,763 ****
  /*
   * get_type_func_class
   *        Given the type OID, obtain its TYPEFUNC classification.
   *
   * This is intended to centralize a bunch of formerly ad-hoc code for
   * classifying types.  The categories used here are useful for deciding
   * how to handle functions returning the datatype.
   */
  static TypeFuncClass
! get_type_func_class(Oid typid)
  {
      switch (get_typtype(typid))
      {
          case TYPTYPE_COMPOSITE:
              return TYPEFUNC_COMPOSITE;
          case TYPTYPE_BASE:
-         case TYPTYPE_DOMAIN:
          case TYPTYPE_ENUM:
          case TYPTYPE_RANGE:
              return TYPEFUNC_SCALAR;
          case TYPTYPE_PSEUDO:
              if (typid == RECORDOID)
                  return TYPEFUNC_RECORD;
--- 786,816 ----
  /*
   * get_type_func_class
   *        Given the type OID, obtain its TYPEFUNC classification.
+  *        Also, if it's a domain, return the base type OID.
   *
   * This is intended to centralize a bunch of formerly ad-hoc code for
   * classifying types.  The categories used here are useful for deciding
   * how to handle functions returning the datatype.
   */
  static TypeFuncClass
! get_type_func_class(Oid typid, Oid *base_typeid)
  {
+     *base_typeid = typid;
+
      switch (get_typtype(typid))
      {
          case TYPTYPE_COMPOSITE:
              return TYPEFUNC_COMPOSITE;
          case TYPTYPE_BASE:
          case TYPTYPE_ENUM:
          case TYPTYPE_RANGE:
              return TYPEFUNC_SCALAR;
+         case TYPTYPE_DOMAIN:
+             *base_typeid = typid = getBaseType(typid);
+             if (get_typtype(typid) == TYPTYPE_COMPOSITE)
+                 return TYPEFUNC_COMPOSITE_DOMAIN;
+             else                /* domain base type can't be a pseudotype */
+                 return TYPEFUNC_SCALAR;
          case TYPTYPE_PSEUDO:
              if (typid == RECORDOID)
                  return TYPEFUNC_RECORD;
*************** RelationNameGetTupleDesc(const char *rel
*** 1320,1335 ****
  TupleDesc
  TypeGetTupleDesc(Oid typeoid, List *colaliases)
  {
!     TypeFuncClass functypclass = get_type_func_class(typeoid);
      TupleDesc    tupdesc = NULL;

      /*
       * Build a suitable tupledesc representing the output rows
       */
!     if (functypclass == TYPEFUNC_COMPOSITE)
      {
          /* Composite data type, e.g. a table's row type */
!         tupdesc = lookup_rowtype_tupdesc_copy(typeoid, -1);

          if (colaliases != NIL)
          {
--- 1373,1390 ----
  TupleDesc
  TypeGetTupleDesc(Oid typeoid, List *colaliases)
  {
!     Oid            base_typeoid;
!     TypeFuncClass functypclass = get_type_func_class(typeoid, &base_typeoid);
      TupleDesc    tupdesc = NULL;

      /*
       * Build a suitable tupledesc representing the output rows
       */
!     if (functypclass == TYPEFUNC_COMPOSITE ||
!         functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
      {
          /* Composite data type, e.g. a table's row type */
!         tupdesc = lookup_rowtype_tupdesc_copy(base_typeoid, -1);

          if (colaliases != NIL)
          {
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index fa04a63..b0d4c54 100644
*** a/src/include/access/htup_details.h
--- b/src/include/access/htup_details.h
*************** typedef struct DatumTupleFields
*** 134,139 ****
--- 134,144 ----
      Oid            datum_typeid;    /* composite type OID, or RECORDOID */

      /*
+      * datum_typeid cannot be a domain over composite, only plain composite,
+      * even if the datum is meant as a value of a domain-over-composite type.
+      * This is in line with the general principle that CoerceToDomain does not
+      * change the physical representation of the base type value.
+      *
       * Note: field ordering is chosen with thought that Oid might someday
       * widen to 64 bits.
       */
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index c15610e..2be5af1 100644
*** a/src/include/access/tupdesc.h
--- b/src/include/access/tupdesc.h
*************** typedef struct tupleConstr
*** 60,65 ****
--- 60,71 ----
   * row type, or a value >= 0 to allow the rowtype to be looked up in the
   * typcache.c type cache.
   *
+  * Note that tdtypeid is never the OID of a domain over composite, even if
+  * we are dealing with values that are known (at some higher level) to be of
+  * a domain-over-composite type.  This is because tdtypeid/tdtypmod need to
+  * match up with the type labeling of composite Datums, and those are never
+  * explicitly marked as being of a domain type, either.
+  *
   * Tuple descriptors that live in caches (relcache or typcache, at present)
   * are reference-counted: they can be deleted when their reference count goes
   * to zero.  Tuple descriptors created by the executor need no reference
diff --git a/src/include/funcapi.h b/src/include/funcapi.h
index 951af2a..7c52de9 100644
*** a/src/include/funcapi.h
--- b/src/include/funcapi.h
*************** typedef struct FuncCallContext
*** 143,148 ****
--- 143,152 ----
   *        get_call_result_type.  Note: the cases in which rowtypes cannot be
   *        determined are different from the cases for get_call_result_type.
   *        Do *not* use this if you can use one of the others.
+  *
+  * See also get_expr_result_tupdesc(), which is a convenient wrapper around
+  * get_expr_result_type() for use when the caller only cares about
+  * determinable-rowtype cases.
   *----------
   */

*************** typedef enum TypeFuncClass
*** 151,156 ****
--- 155,161 ----
  {
      TYPEFUNC_SCALAR,            /* scalar result type */
      TYPEFUNC_COMPOSITE,            /* determinable rowtype result */
+     TYPEFUNC_COMPOSITE_DOMAIN,    /* domain over determinable rowtype result */
      TYPEFUNC_RECORD,            /* indeterminate rowtype result */
      TYPEFUNC_OTHER                /* bogus type, eg pseudotype */
  } TypeFuncClass;
*************** extern TypeFuncClass get_func_result_typ
*** 165,170 ****
--- 170,177 ----
                       Oid *resultTypeId,
                       TupleDesc *resultTupleDesc);

+ extern TupleDesc get_expr_result_tupdesc(Node *expr, bool noError);
+
  extern bool resolve_polymorphic_argtypes(int numargs, Oid *argtypes,
                               char *argmodes,
                               Node *call_expr);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index ccb5123..c2929ac 100644
*** a/src/include/nodes/primnodes.h
--- b/src/include/nodes/primnodes.h
*************** typedef struct Var
*** 166,172 ****
      Index        varno;            /* index of this var's relation in the range
                                   * table, or INNER_VAR/OUTER_VAR/INDEX_VAR */
      AttrNumber    varattno;        /* attribute number of this var, or zero for
!                                  * all */
      Oid            vartype;        /* pg_type OID for the type of this var */
      int32        vartypmod;        /* pg_attribute typmod value */
      Oid            varcollid;        /* OID of collation, or InvalidOid if none */
--- 166,172 ----
      Index        varno;            /* index of this var's relation in the range
                                   * table, or INNER_VAR/OUTER_VAR/INDEX_VAR */
      AttrNumber    varattno;        /* attribute number of this var, or zero for
!                                  * all attrs ("whole-row Var") */
      Oid            vartype;        /* pg_type OID for the type of this var */
      int32        vartypmod;        /* pg_attribute typmod value */
      Oid            varcollid;        /* OID of collation, or InvalidOid if none */
*************** typedef struct FieldSelect
*** 755,760 ****
--- 755,763 ----
   * the assign case of ArrayRef, this is used to implement UPDATE of a
   * portion of a column.
   *
+  * resulttype is always a named composite type (not a domain).  To update
+  * a composite domain value, apply CoerceToDomain to the FieldStore.
+  *
   * A single FieldStore can actually represent updates of several different
   * fields.  The parser only generates FieldStores with single-element lists,
   * but the planner will collapse multiple updates of the same base column
*************** typedef struct ArrayCoerceExpr
*** 849,855 ****
   * needed for the destination type plus possibly others; the columns need not
   * be in the same positions, but are matched up by name.  This is primarily
   * used to convert a whole-row value of an inheritance child table into a
!  * valid whole-row value of its parent table's rowtype.
   * ----------------
   */

--- 852,859 ----
   * needed for the destination type plus possibly others; the columns need not
   * be in the same positions, but are matched up by name.  This is primarily
   * used to convert a whole-row value of an inheritance child table into a
!  * valid whole-row value of its parent table's rowtype.  Both resulttype
!  * and the exposed type of "arg" must be named composite types (not domains).
   * ----------------
   */

*************** typedef struct RowExpr
*** 987,992 ****
--- 991,999 ----
      Oid            row_typeid;        /* RECORDOID or a composite type's ID */

      /*
+      * row_typeid cannot be a domain over composite, only plain composite.  To
+      * create a composite domain value, apply CoerceToDomain to the RowExpr.
+      *
       * Note: we deliberately do NOT store a typmod.  Although a typmod will be
       * associated with specific RECORD types at runtime, it will differ for
       * different backends, and so cannot safely be stored in stored
diff --git a/src/include/parser/parse_type.h b/src/include/parser/parse_type.h
index 7b843d0..af1e314 100644
*** a/src/include/parser/parse_type.h
--- b/src/include/parser/parse_type.h
*************** extern Oid    typeTypeCollation(Type typ);
*** 46,55 ****
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! #define ISCOMPLEX(typeid) (typeidTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
--- 46,57 ----
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);
+ extern Oid    typeOrDomainTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! /* true if typeid is composite, or domain over composite, but not RECORD */
! #define ISCOMPLEX(typeid) (typeOrDomainTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 41b645a..ea799a8 100644
*** a/src/include/utils/typcache.h
--- b/src/include/utils/typcache.h
*************** typedef struct TypeCacheEntry
*** 92,97 ****
--- 92,104 ----
      FmgrInfo    rng_subdiff_finfo;    /* difference function, if any */

      /*
+      * Domain's base type and typmod if it's a domain type.  Zeroes if not
+      * domain, or if information hasn't been requested.
+      */
+     Oid            domainBaseType;
+     int32        domainBaseTypmod;
+
+     /*
       * Domain constraint data if it's a domain type.  NULL if not domain, or
       * if domain has no constraints, or if information hasn't been requested.
       */
*************** typedef struct TypeCacheEntry
*** 123,131 ****
  #define TYPECACHE_BTREE_OPFAMILY    0x0200
  #define TYPECACHE_HASH_OPFAMILY        0x0400
  #define TYPECACHE_RANGE_INFO        0x0800
! #define TYPECACHE_DOMAIN_INFO        0x1000
! #define TYPECACHE_HASH_EXTENDED_PROC        0x2000
! #define TYPECACHE_HASH_EXTENDED_PROC_FINFO    0x4000

  /*
   * Callers wishing to maintain a long-lived reference to a domain's constraint
--- 130,139 ----
  #define TYPECACHE_BTREE_OPFAMILY    0x0200
  #define TYPECACHE_HASH_OPFAMILY        0x0400
  #define TYPECACHE_RANGE_INFO        0x0800
! #define TYPECACHE_DOMAIN_BASE_INFO            0x1000
! #define TYPECACHE_DOMAIN_CONSTR_INFO        0x2000
! #define TYPECACHE_HASH_EXTENDED_PROC        0x4000
! #define TYPECACHE_HASH_EXTENDED_PROC_FINFO    0x8000

  /*
   * Callers wishing to maintain a long-lived reference to a domain's constraint
*************** extern TupleDesc lookup_rowtype_tupdesc_
*** 163,168 ****
--- 171,179 ----

  extern TupleDesc lookup_rowtype_tupdesc_copy(Oid type_id, int32 typmod);

+ extern TupleDesc lookup_rowtype_tupdesc_domain(Oid type_id, int32 typmod,
+                               bool noError);
+
  extern void assign_record_type_typmod(TupleDesc tupDesc);

  extern int    compare_values_of_enum(TypeCacheEntry *tcache, Oid arg1, Oid arg2);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 1e62c57..f7f3948 100644
*** a/src/test/regress/expected/domain.out
--- b/src/test/regress/expected/domain.out
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 198,203 ****
--- 198,291 ----
  (1 row)

  drop domain dia;
+ -- Test domains over composites
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ ERROR:  duplicate key value violates unique constraint "dcomptable_d1_key"
+ DETAIL:  Key (d1)=((1,2)) already exists.
+ insert into dcomptable (d1.r) values(11);
+ select * from dcomptable;
+   d1
+ -------
+  (1,2)
+  (3,4)
+  (11,)
+ (3 rows)
+
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+  r  | i | r  | i
+ ----+---+----+---
+   1 | 2 |  1 | 2
+   3 | 4 |  3 | 4
+  11 |   | 11 |
+ (3 rows)
+
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+   d1
+ -------
+  (11,)
+  (2,2)
+  (4,4)
+ (3 rows)
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+ ERROR:  column "d1" of table "dcomptable" contains values that violate the new constraint
+ select row(2,1)::dcomptype;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+     d1
+ ----------
+  (11,)
+  (99,)
+  (1,3)
+  (3,5)
+  (0,3)
+  (98,101)
+ (6 rows)
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+                                           QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+  Update on public.dcomptable
+    ->  Seq Scan on public.dcomptable
+          Output: ROW(((d1).r - '1'::double precision), ((d1).i + '1'::double precision)), ctid
+          Filter: ((dcomptable.d1).i > '0'::double precision)
+ (4 rows)
+
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+                                   Table "public.dcomptable"
+  Column |   Type    | Collation | Nullable | Default | Storage  | Stats target | Description
+ --------+-----------+-----------+----------+---------+----------+--------------+-------------
+  d1     | dcomptype |           |          |         | extended |              |
+ Indexes:
+     "dcomptable_d1_key" UNIQUE CONSTRAINT, btree (d1)
+ Rules:
+     silly AS
+     ON DELETE TO dcomptable DO INSTEAD  UPDATE dcomptable SET d1.r = (dcomptable.d1).r - 1::double precision, d1.i =
(dcomptable.d1).i+ 1::double precision 
+   WHERE (dcomptable.d1).i > 0::double precision
+
+ drop table dcomptable;
+ drop type comptype cascade;
+ NOTICE:  drop cascades to type dcomptype
  -- Test domains over arrays of composite
  create type comptype as (r float8, i float8);
  create domain dcomptypea as comptype[];
*************** insert into ddtest2 values('{(-1)}');
*** 762,767 ****
--- 850,863 ----
  alter domain posint add constraint c1 check(value >= 0);
  ERROR:  cannot alter type "posint" because column "ddtest2.f1" uses it
  drop table ddtest2;
+ -- Likewise for domains within domains over composite
+ create domain ddtest1d as ddtest1;
+ create table ddtest2(f1 ddtest1d);
+ insert into ddtest2 values('(-1)');
+ alter domain posint add constraint c1 check(value >= 0);
+ ERROR:  cannot alter type "posint" because column "ddtest2.f1" uses it
+ drop table ddtest2;
+ drop domain ddtest1d;
  -- Likewise for domains within domains over array of composite
  create domain ddtest1d as ddtest1[];
  create table ddtest2(f1 ddtest1d);
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 8fb3e20..5201f00 100644
*** a/src/test/regress/sql/domain.sql
--- b/src/test/regress/sql/domain.sql
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 120,125 ****
--- 120,164 ----
  drop domain dia;


+ -- Test domains over composites
+
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ insert into dcomptable (d1.r) values(11);
+
+ select * from dcomptable;
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+
+ select row(2,1)::dcomptype;  -- fail
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+
+ drop table dcomptable;
+ drop type comptype cascade;
+
+
  -- Test domains over arrays of composite

  create type comptype as (r float8, i float8);
*************** insert into ddtest2 values('{(-1)}');
*** 500,505 ****
--- 539,552 ----
  alter domain posint add constraint c1 check(value >= 0);
  drop table ddtest2;

+ -- Likewise for domains within domains over composite
+ create domain ddtest1d as ddtest1;
+ create table ddtest2(f1 ddtest1d);
+ insert into ddtest2 values('(-1)');
+ alter domain posint add constraint c1 check(value >= 0);
+ drop table ddtest2;
+ drop domain ddtest1d;
+
  -- Likewise for domains within domains over array of composite
  create domain ddtest1d as ddtest1[];
  create table ddtest2(f1 ddtest1d);

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Re: [HACKERS] Domains and arrays and composites, oh my

From
Tom Lane
Date:
I wrote:
> Anyway, PFA an updated patch that also fixes some conflicts with the
> already-committed arrays-of-domains patch.

I realized that the pending patch for jsonb_build_object doesn't
actually have any conflict with what I needed to touch here, so
I went ahead and fixed the JSON functions that needed fixing,
along with hstore's populate_record.  I ended up rewriting the
argument-metadata-collection portions of populate_record_worker
and populate_recordset_worker rather heavily, because I didn't
like them at all: aside from not working for domains over composite,
they were pretty inefficient (redoing a lot of work on each call
for no good reason) and they were randomly different from each
other, resulting in json{b}_populate_recordset rejecting some cases
that worked in json{b}_populate_record.

I've also updated the documentation.

I think that this patch version is done so far as the core code
and contrib are concerned.  The PLs need varying amounts of work,
but as I said earlier, I think it would be better to tackle those
in separate patches instead of continuing to enlarge the footprint
of the core patch.  So, barring objection, I'd like to go ahead
and commit this.

            regards, tom lane

diff --git a/contrib/hstore/hstore_io.c b/contrib/hstore/hstore_io.c
index d828401..e999a8e 100644
*** a/contrib/hstore/hstore_io.c
--- b/contrib/hstore/hstore_io.c
*************** typedef struct RecordIOData
*** 752,757 ****
--- 752,759 ----
  {
      Oid            record_type;
      int32        record_typmod;
+     /* this field is used only if target type is domain over composite: */
+     void       *domain_info;    /* opaque cache for domain checks */
      int            ncolumns;
      ColumnIOData columns[FLEXIBLE_ARRAY_MEMBER];
  } RecordIOData;
*************** hstore_from_record(PG_FUNCTION_ARGS)
*** 780,788 ****
          Oid            argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);

          /*
!          * have no tuple to look at, so the only source of type info is the
!          * argtype. The lookup_rowtype_tupdesc call below will error out if we
!          * don't have a known composite type oid here.
           */
          tupType = argtype;
          tupTypmod = -1;
--- 782,792 ----
          Oid            argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);

          /*
!          * We have no tuple to look at, so the only source of type info is the
!          * argtype --- which might be domain over composite, but we don't care
!          * here, since we have no need to be concerned about domain
!          * constraints.  The lookup_rowtype_tupdesc_domain call below will
!          * error out if we don't have a known composite type oid here.
           */
          tupType = argtype;
          tupTypmod = -1;
*************** hstore_from_record(PG_FUNCTION_ARGS)
*** 793,804 ****
      {
          rec = PG_GETARG_HEAPTUPLEHEADER(0);

!         /* Extract type info from the tuple itself */
          tupType = HeapTupleHeaderGetTypeId(rec);
          tupTypmod = HeapTupleHeaderGetTypMod(rec);
      }

!     tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
      ncolumns = tupdesc->natts;

      /*
--- 797,811 ----
      {
          rec = PG_GETARG_HEAPTUPLEHEADER(0);

!         /*
!          * Extract type info from the tuple itself -- this will work even for
!          * anonymous record types.
!          */
          tupType = HeapTupleHeaderGetTypeId(rec);
          tupTypmod = HeapTupleHeaderGetTypMod(rec);
      }

!     tupdesc = lookup_rowtype_tupdesc_domain(tupType, tupTypmod, false);
      ncolumns = tupdesc->natts;

      /*
*************** hstore_populate_record(PG_FUNCTION_ARGS)
*** 943,951 ****
          rec = NULL;

          /*
!          * have no tuple to look at, so the only source of type info is the
!          * argtype. The lookup_rowtype_tupdesc call below will error out if we
!          * don't have a known composite type oid here.
           */
          tupType = argtype;
          tupTypmod = -1;
--- 950,958 ----
          rec = NULL;

          /*
!          * We have no tuple to look at, so the only source of type info is the
!          * argtype.  The lookup_rowtype_tupdesc_domain call below will error
!          * out if we don't have a known composite type oid here.
           */
          tupType = argtype;
          tupTypmod = -1;
*************** hstore_populate_record(PG_FUNCTION_ARGS)
*** 957,963 ****
          if (PG_ARGISNULL(1))
              PG_RETURN_POINTER(rec);

!         /* Extract type info from the tuple itself */
          tupType = HeapTupleHeaderGetTypeId(rec);
          tupTypmod = HeapTupleHeaderGetTypMod(rec);
      }
--- 964,973 ----
          if (PG_ARGISNULL(1))
              PG_RETURN_POINTER(rec);

!         /*
!          * Extract type info from the tuple itself -- this will work even for
!          * anonymous record types.
!          */
          tupType = HeapTupleHeaderGetTypeId(rec);
          tupTypmod = HeapTupleHeaderGetTypMod(rec);
      }
*************** hstore_populate_record(PG_FUNCTION_ARGS)
*** 975,981 ****
      if (HS_COUNT(hs) == 0 && rec)
          PG_RETURN_POINTER(rec);

!     tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
      ncolumns = tupdesc->natts;

      if (rec)
--- 985,995 ----
      if (HS_COUNT(hs) == 0 && rec)
          PG_RETURN_POINTER(rec);

!     /*
!      * Lookup the input record's tupdesc.  For the moment, we don't worry
!      * about whether it is a domain over composite.
!      */
!     tupdesc = lookup_rowtype_tupdesc_domain(tupType, tupTypmod, false);
      ncolumns = tupdesc->natts;

      if (rec)
*************** hstore_populate_record(PG_FUNCTION_ARGS)
*** 1002,1007 ****
--- 1016,1022 ----
          my_extra = (RecordIOData *) fcinfo->flinfo->fn_extra;
          my_extra->record_type = InvalidOid;
          my_extra->record_typmod = 0;
+         my_extra->domain_info = NULL;
      }

      if (my_extra->record_type != tupType ||
*************** hstore_populate_record(PG_FUNCTION_ARGS)
*** 1103,1108 ****
--- 1118,1134 ----

      rettuple = heap_form_tuple(tupdesc, values, nulls);

+     /*
+      * If the target type is domain over composite, all we know at this point
+      * is that we've made a valid value of the base composite type.  Must
+      * check domain constraints before deciding we're done.
+      */
+     if (argtype != tupdesc->tdtypeid)
+         domain_check(HeapTupleGetDatum(rettuple), false,
+                      argtype,
+                      &my_extra->domain_info,
+                      fcinfo->flinfo->fn_mcxt);
+
      ReleaseTupleDesc(tupdesc);

      PG_RETURN_DATUM(HeapTupleGetDatum(rettuple));
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index b397e18..3d46098 100644
*** a/doc/src/sgml/datatype.sgml
--- b/doc/src/sgml/datatype.sgml
*************** SET xmloption TO { DOCUMENT | CONTENT };
*** 4379,4386 ****
      underlying type — for example, any operator or function that
      can be applied to the underlying type will work on the domain type.
      The underlying type can be any built-in or user-defined base type,
!     enum type, array or range type, or another domain.  (Currently, domains
!     over composite types are not implemented.)
     </para>

     <para>
--- 4379,4385 ----
      underlying type — for example, any operator or function that
      can be applied to the underlying type will work on the domain type.
      The underlying type can be any built-in or user-defined base type,
!     enum type, array type, composite type, range type, or another domain.
     </para>

     <para>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
index 7e436a4..f80d44b 100644
*** a/doc/src/sgml/rowtypes.sgml
--- b/doc/src/sgml/rowtypes.sgml
*************** CREATE TABLE inventory_item (
*** 84,91 ****
    restriction of the current implementation: since no constraints are
    associated with a composite type, the constraints shown in the table
    definition <emphasis>do not apply</emphasis> to values of the composite type
!   outside the table.  (A partial workaround is to use domain
!   types as members of composite types.)
   </para>
   </sect2>

--- 84,92 ----
    restriction of the current implementation: since no constraints are
    associated with a composite type, the constraints shown in the table
    definition <emphasis>do not apply</emphasis> to values of the composite type
!   outside the table.  (To work around this, create a domain over the composite
!   type, and apply the desired constraints as <literal>CHECK</literal>
!   constraints of the domain.)
   </para>
   </sect2>

diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d9fccaa..9bdb72c 100644
*** a/doc/src/sgml/xfunc.sgml
--- b/doc/src/sgml/xfunc.sgml
*************** CREATE FUNCTION tf1 (accountno integer,
*** 353,358 ****
--- 353,383 ----
  $$ LANGUAGE SQL;
  </programlisting>
      </para>
+
+     <para>
+      A <acronym>SQL</acronym> function must return exactly its declared
+      result type.  This may require inserting an explicit cast.
+      For example, suppose we wanted the
+      previous <function>add_em</function> function to return
+      type <type>float8</type> instead.  This won't work:
+
+ <programlisting>
+ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
+     SELECT $1 + $2;
+ $$ LANGUAGE SQL;
+ </programlisting>
+
+      even though in other contexts <productname>PostgreSQL</productname>
+      would be willing to insert an implicit cast to
+      convert <type>integer</type> to <type>float8</type>.
+      We need to write it as
+
+ <programlisting>
+ CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
+     SELECT ($1 + $2)::float8;
+ $$ LANGUAGE SQL;
+ </programlisting>
+     </para>
     </sect2>

     <sect2 id="xfunc-sql-composite-functions">
*************** $$ LANGUAGE SQL;
*** 452,464 ****
        </listitem>
        <listitem>
         <para>
!         You must typecast the expressions to match the
!         definition of the composite type, or you will get errors like this:
  <screen>
  <computeroutput>
  ERROR:  function declared to return emp returns varchar instead of text at column 1
  </computeroutput>
  </screen>
         </para>
        </listitem>
       </itemizedlist>
--- 477,492 ----
        </listitem>
        <listitem>
         <para>
!         We must ensure each expression's type matches the corresponding
!         column of the composite type, inserting a cast if necessary.
!         Otherwise we'll get errors like this:
  <screen>
  <computeroutput>
  ERROR:  function declared to return emp returns varchar instead of text at column 1
  </computeroutput>
  </screen>
+         As with the base-type case, the function will not insert any casts
+         automatically.
         </para>
        </listitem>
       </itemizedlist>
*************** $$ LANGUAGE SQL;
*** 478,483 ****
--- 506,516 ----
       in this situation, but it is a handy alternative in some cases
       — for example, if we need to compute the result by calling
       another function that returns the desired composite value.
+      Another example is that if we are trying to write a function that
+      returns a domain over composite, rather than a plain composite type,
+      it is always necessary to write it as returning a single column,
+      since there is no other way to produce a value that is exactly of
+      the domain type.
      </para>

      <para>
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 245a374..1bd8a58 100644
*** a/src/backend/catalog/pg_inherits.c
--- b/src/backend/catalog/pg_inherits.c
*************** has_superclass(Oid relationId)
*** 301,306 ****
--- 301,311 ----
  /*
   * Given two type OIDs, determine whether the first is a complex type
   * (class type) that inherits from the second.
+  *
+  * This essentially asks whether the first type is guaranteed to be coercible
+  * to the second.  Therefore, we allow the first type to be a domain over a
+  * complex type that inherits from the second; that creates no difficulties.
+  * But the second type cannot be a domain.
   */
  bool
  typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId)
*************** typeInheritsFrom(Oid subclassTypeId, Oid
*** 314,322 ****
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeidTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
--- 319,327 ----
      ListCell   *queue_item;

      /* We need to work with the associated relation OIDs */
!     subclassRelid = typeOrDomainTypeRelid(subclassTypeId);
      if (subclassRelid == InvalidOid)
!         return false;            /* not a complex type or domain over one */
      superclassRelid = typeidTypeRelid(superclassTypeId);
      if (superclassRelid == InvalidOid)
          return false;            /* not a complex type */
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 571856e..47916cf 100644
*** a/src/backend/catalog/pg_proc.c
--- b/src/backend/catalog/pg_proc.c
*************** ProcedureCreate(const char *procedureNam
*** 262,268 ****
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeidTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
--- 262,268 ----
       */
      if (parameterCount == 1 &&
          OidIsValid(parameterTypes->values[0]) &&
!         (relid = typeOrDomainTypeRelid(parameterTypes->values[0])) != InvalidOid &&
          get_attnum(relid, procedureName) != InvalidAttrNumber)
          ereport(ERROR,
                  (errcode(ERRCODE_DUPLICATE_COLUMN),
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2d4dcd7..09a7001 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** find_typed_table_dependencies(Oid typeOi
*** 5091,5096 ****
--- 5091,5098 ----
   * isn't suitable, throw an error.  Currently, we require that the type
   * originated with CREATE TYPE AS.  We could support any row type, but doing so
   * would require handling a number of extra corner cases in the DDL commands.
+  * (Also, allowing domain-over-composite would open up a can of worms about
+  * whether and how the domain's constraints should apply to derived tables.)
   */
  void
  check_of_type(HeapTuple typetuple)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index c1b87e0..7df942b 100644
*** a/src/backend/commands/typecmds.c
--- b/src/backend/commands/typecmds.c
*************** DefineDomain(CreateDomainStmt *stmt)
*** 798,810 ****
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, another domain, an enum or a range
!      * type. Domains over pseudotypes would create a security hole.  Domains
!      * over composite types might be made to work in the future, but not
!      * today.
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
--- 798,813 ----
      basetypeoid = HeapTupleGetOid(typeTup);

      /*
!      * Base type must be a plain base type, a composite type, another domain,
!      * an enum or a range type.  Domains over pseudotypes would create a
!      * security hole.  (It would be shorter to code this to just check for
!      * pseudotypes; but it seems safer to call out the specific typtypes that
!      * are supported, rather than assume that all future typtypes would be
!      * automatically supported.)
       */
      typtype = baseType->typtype;
      if (typtype != TYPTYPE_BASE &&
+         typtype != TYPTYPE_COMPOSITE &&
          typtype != TYPTYPE_DOMAIN &&
          typtype != TYPTYPE_ENUM &&
          typtype != TYPTYPE_RANGE)
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index c5e97ef..a0f537b 100644
*** a/src/backend/executor/execExprInterp.c
--- b/src/backend/executor/execExprInterp.c
*************** ExecEvalWholeRowVar(ExprState *state, Ex
*** 3469,3476 ****
               * generates an INT4 NULL regardless of the dropped column type).
               * If we find a dropped column and cannot verify that case (1)
               * holds, we have to use the slow path to check (2) for each row.
               */
!             var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1);

              slot_tupdesc = slot->tts_tupleDescriptor;

--- 3469,3480 ----
               * generates an INT4 NULL regardless of the dropped column type).
               * If we find a dropped column and cannot verify that case (1)
               * holds, we have to use the slow path to check (2) for each row.
+              *
+              * If vartype is a domain over composite, just look through that
+              * to the base composite type.
               */
!             var_tupdesc = lookup_rowtype_tupdesc_domain(variable->vartype,
!                                                         -1, false);

              slot_tupdesc = slot->tts_tupleDescriptor;

diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c
index cce771d..aabe26c 100644
*** a/src/backend/executor/execSRF.c
--- b/src/backend/executor/execSRF.c
*************** init_sexpr(Oid foid, Oid input_collation
*** 734,740 ****
          /* Must save tupdesc in sexpr's context */
          oldcontext = MemoryContextSwitchTo(sexprCxt);

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 734,741 ----
          /* Must save tupdesc in sexpr's context */
          oldcontext = MemoryContextSwitchTo(sexprCxt);

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 42a4ca9..98eb777 100644
*** a/src/backend/executor/functions.c
--- b/src/backend/executor/functions.c
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1665,1671 ****
      }
      else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID)
      {
!         /* Returns a rowtype */
          TupleDesc    tupdesc;
          int            tupnatts;    /* physical number of columns in tuple */
          int            tuplogcols; /* # of nondeleted columns in tuple */
--- 1665,1679 ----
      }
      else if (fn_typtype == TYPTYPE_COMPOSITE || rettype == RECORDOID)
      {
!         /*
!          * Returns a rowtype.
!          *
!          * Note that we will not consider a domain over composite to be a
!          * "rowtype" return type; it goes through the scalar case above.  This
!          * is because SQL functions don't provide any implicit casting to the
!          * result type, so there is no way to produce a domain-over-composite
!          * result except by computing it as an explicit single-column result.
!          */
          TupleDesc    tupdesc;
          int            tupnatts;    /* physical number of columns in tuple */
          int            tuplogcols; /* # of nondeleted columns in tuple */
*************** check_sql_fn_retval(Oid func_id, Oid ret
*** 1711,1717 ****
              }
          }

!         /* Is the rowtype fixed, or determined only at runtime? */
          if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          {
              /*
--- 1719,1728 ----
              }
          }

!         /*
!          * Is the rowtype fixed, or determined only at runtime?  (Note we
!          * cannot see TYPEFUNC_COMPOSITE_DOMAIN here.)
!          */
          if (get_func_result_type(func_id, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          {
              /*
diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c
index 9f87a7e..de476ac 100644
*** a/src/backend/executor/nodeFunctionscan.c
--- b/src/backend/executor/nodeFunctionscan.c
*************** ExecInitFunctionScan(FunctionScan *node,
*** 383,389 ****
                                              &funcrettype,
                                              &tupdesc);

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 383,390 ----
                                              &funcrettype,
                                              &tupdesc);

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index b58eb0f..7a67653 100644
*** a/src/backend/nodes/makefuncs.c
--- b/src/backend/nodes/makefuncs.c
*************** makeVarFromTargetEntry(Index varno,
*** 120,127 ****
   * table entry, and varattno == 0 to signal that it references the whole
   * tuple.  (Use of zero here is unclean, since it could easily be confused
   * with error cases, but it's not worth changing now.)  The vartype indicates
!  * a rowtype; either a named composite type, or RECORD.  This function
!  * encapsulates the logic for determining the correct rowtype OID to use.
   *
   * If allowScalar is true, then for the case where the RTE is a single function
   * returning a non-composite result type, we produce a normal Var referencing
--- 120,129 ----
   * table entry, and varattno == 0 to signal that it references the whole
   * tuple.  (Use of zero here is unclean, since it could easily be confused
   * with error cases, but it's not worth changing now.)  The vartype indicates
!  * a rowtype; either a named composite type, or a domain over a named
!  * composite type (only possible if the RTE is a function returning that),
!  * or RECORD.  This function encapsulates the logic for determining the
!  * correct rowtype OID to use.
   *
   * If allowScalar is true, then for the case where the RTE is a single function
   * returning a non-composite result type, we produce a normal Var referencing
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 7961362..5344f61 100644
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
*************** CommuteRowCompareExpr(RowCompareExpr *cl
*** 2356,2361 ****
--- 2356,2365 ----
   * is still what it was when the expression was parsed.  This is needed to
   * guard against improper simplification after ALTER COLUMN TYPE.  (XXX we
   * may well need to make similar checks elsewhere?)
+  *
+  * rowtypeid may come from a whole-row Var, and therefore it can be a domain
+  * over composite, but for this purpose we only care about checking the type
+  * of a contained field.
   */
  static bool
  rowtype_field_matches(Oid rowtypeid, int fieldnum,
*************** rowtype_field_matches(Oid rowtypeid, int
*** 2368,2374 ****
      /* No issue for RECORD, since there is no way to ALTER such a type */
      if (rowtypeid == RECORDOID)
          return true;
!     tupdesc = lookup_rowtype_tupdesc(rowtypeid, -1);
      if (fieldnum <= 0 || fieldnum > tupdesc->natts)
      {
          ReleaseTupleDesc(tupdesc);
--- 2372,2378 ----
      /* No issue for RECORD, since there is no way to ALTER such a type */
      if (rowtypeid == RECORDOID)
          return true;
!     tupdesc = lookup_rowtype_tupdesc_domain(rowtypeid, -1, false);
      if (fieldnum <= 0 || fieldnum > tupdesc->natts)
      {
          ReleaseTupleDesc(tupdesc);
*************** inline_set_returning_function(PlannerInf
*** 5005,5011 ****
       *
       * If the function returns a composite type, don't inline unless the check
       * shows it's returning a whole tuple result; otherwise what it's
!      * returning is a single composite column which is not what we need.
       */
      if (!check_sql_fn_retval(func_oid, fexpr->funcresulttype,
                               querytree_list,
--- 5009,5017 ----
       *
       * If the function returns a composite type, don't inline unless the check
       * shows it's returning a whole tuple result; otherwise what it's
!      * returning is a single composite column which is not what we need. (Like
!      * check_sql_fn_retval, we deliberately exclude domains over composite
!      * here.)
       */
      if (!check_sql_fn_retval(func_oid, fexpr->funcresulttype,
                               querytree_list,
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 53457dc..def41b3 100644
*** a/src/backend/parser/parse_coerce.c
--- b/src/backend/parser/parse_coerce.c
*************** coerce_type(ParseState *pstate, Node *no
*** 499,507 ****
--- 499,524 ----
           * Input class type is a subclass of target, so generate an
           * appropriate runtime conversion (removing unneeded columns and
           * possibly rearranging the ones that are wanted).
+          *
+          * We will also get here when the input is a domain over a subclass of
+          * the target type.  To keep life simple for the executor, we define
+          * ConvertRowtypeExpr as only working between regular composite types;
+          * therefore, in such cases insert a RelabelType to smash the input
+          * expression down to its base type.
           */
+         Oid            baseTypeId = getBaseType(inputTypeId);
          ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);

+         if (baseTypeId != inputTypeId)
+         {
+             RelabelType *rt = makeRelabelType((Expr *) node,
+                                               baseTypeId, -1,
+                                               InvalidOid,
+                                               COERCE_IMPLICIT_CAST);
+
+             rt->location = location;
+             node = (Node *) rt;
+         }
          r->arg = (Expr *) node;
          r->resulttype = targetTypeId;
          r->convertformat = cformat;
*************** coerce_record_to_complex(ParseState *pst
*** 966,971 ****
--- 983,990 ----
                           int location)
  {
      RowExpr    *rowexpr;
+     Oid            baseTypeId;
+     int32        baseTypeMod = -1;
      TupleDesc    tupdesc;
      List       *args = NIL;
      List       *newargs;
*************** coerce_record_to_complex(ParseState *pst
*** 1001,1007 ****
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     tupdesc = lookup_rowtype_tupdesc(targetTypeId, -1);
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
--- 1020,1033 ----
                          format_type_be(targetTypeId)),
                   parser_coercion_errposition(pstate, location, node)));

!     /*
!      * Look up the composite type, accounting for possibility that what we are
!      * given is a domain over composite.
!      */
!     baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!     tupdesc = lookup_rowtype_tupdesc(baseTypeId, baseTypeMod);
!
!     /* Process the fields */
      newargs = NIL;
      ucolno = 1;
      arg = list_head(args);
*************** coerce_record_to_complex(ParseState *pst
*** 1070,1079 ****

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = targetTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
      return (Node *) rowexpr;
  }

--- 1096,1117 ----

      rowexpr = makeNode(RowExpr);
      rowexpr->args = newargs;
!     rowexpr->row_typeid = baseTypeId;
      rowexpr->row_format = cformat;
      rowexpr->colnames = NIL;    /* not needed for named target type */
      rowexpr->location = location;
+
+     /* If target is a domain, apply constraints */
+     if (baseTypeId != targetTypeId)
+     {
+         rowexpr->row_format = COERCE_IMPLICIT_CAST;
+         return coerce_to_domain((Node *) rowexpr,
+                                 baseTypeId, baseTypeMod,
+                                 targetTypeId,
+                                 ccontext, cformat, location,
+                                 false);
+     }
+
      return (Node *) rowexpr;
  }

*************** is_complex_array(Oid typid)
*** 2401,2413 ****

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId.  (This is conceptually similar to the subtype
!  * relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeidTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
--- 2439,2451 ----

  /*
   * Check whether reltypeId is the row type of a typed table of type
!  * reloftypeId, or is a domain over such a row type.  (This is conceptually
!  * similar to the subtype relationship checked by typeInheritsFrom().)
   */
  static bool
  typeIsOfTypedTable(Oid reltypeId, Oid reloftypeId)
  {
!     Oid            relid = typeOrDomainTypeRelid(reltypeId);
      bool        result = false;

      if (relid)
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 2f2f2c7..fc0d6bc 100644
*** a/src/backend/parser/parse_func.c
--- b/src/backend/parser/parse_func.c
*************** ParseComplexProjection(ParseState *pstat
*** 1819,1836 ****
      }

      /*
!      * Else do it the hard way with get_expr_result_type().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else if (get_expr_result_type(first_arg, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
          return NULL;            /* unresolvable RECORD type */
-     Assert(tupdesc);

      for (i = 0; i < tupdesc->natts; i++)
      {
--- 1819,1837 ----
      }

      /*
!      * Else do it the hard way with get_expr_result_tupdesc().
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(first_arg, Var) &&
          ((Var *) first_arg)->vartype == RECORDOID)
          tupdesc = expandRecordVariable(pstate, (Var *) first_arg, 0);
!     else
!         tupdesc = get_expr_result_tupdesc(first_arg, true);
!     if (!tupdesc)
          return NULL;            /* unresolvable RECORD type */

      for (i = 0; i < tupdesc->natts; i++)
      {
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index a9273af..ca32a37 100644
*** a/src/backend/parser/parse_relation.c
--- b/src/backend/parser/parse_relation.c
*************** addRangeTableEntryForFunction(ParseState
*** 1496,1502 ****
                           parser_errposition(pstate, exprLocation(funcexpr))));
          }

!         if (functypclass == TYPEFUNC_COMPOSITE)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
--- 1496,1503 ----
                           parser_errposition(pstate, exprLocation(funcexpr))));
          }

!         if (functypclass == TYPEFUNC_COMPOSITE ||
!             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
          {
              /* Composite data type, e.g. a table's row type */
              Assert(tupdesc);
*************** expandRTE(RangeTblEntry *rte, int rtinde
*** 2245,2251 ****
                      functypclass = get_expr_result_type(rtfunc->funcexpr,
                                                          &funcrettype,
                                                          &tupdesc);
!                     if (functypclass == TYPEFUNC_COMPOSITE)
                      {
                          /* Composite data type, e.g. a table's row type */
                          Assert(tupdesc);
--- 2246,2253 ----
                      functypclass = get_expr_result_type(rtfunc->funcexpr,
                                                          &funcrettype,
                                                          &tupdesc);
!                     if (functypclass == TYPEFUNC_COMPOSITE ||
!                         functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
                      {
                          /* Composite data type, e.g. a table's row type */
                          Assert(tupdesc);
*************** get_rte_attribute_type(RangeTblEntry *rt
*** 2765,2771 ****
                                                              &funcrettype,
                                                              &tupdesc);

!                         if (functypclass == TYPEFUNC_COMPOSITE)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
--- 2767,2774 ----
                                                              &funcrettype,
                                                              &tupdesc);

!                         if (functypclass == TYPEFUNC_COMPOSITE ||
!                             functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
*************** get_rte_attribute_is_dropped(RangeTblEnt
*** 2966,2979 ****
                      if (attnum > atts_done &&
                          attnum <= atts_done + rtfunc->funccolcount)
                      {
-                         TypeFuncClass functypclass;
-                         Oid            funcrettype;
                          TupleDesc    tupdesc;

!                         functypclass = get_expr_result_type(rtfunc->funcexpr,
!                                                             &funcrettype,
!                                                             &tupdesc);
!                         if (functypclass == TYPEFUNC_COMPOSITE)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
--- 2969,2979 ----
                      if (attnum > atts_done &&
                          attnum <= atts_done + rtfunc->funccolcount)
                      {
                          TupleDesc    tupdesc;

!                         tupdesc = get_expr_result_tupdesc(rtfunc->funcexpr,
!                                                           true);
!                         if (tupdesc)
                          {
                              /* Composite data type, e.g. a table's row type */
                              Form_pg_attribute att_tup;
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 2547524..16d3dba 100644
*** a/src/backend/parser/parse_target.c
--- b/src/backend/parser/parse_target.c
*************** transformAssignmentIndirection(ParseStat
*** 725,730 ****
--- 725,732 ----
          else
          {
              FieldStore *fstore;
+             Oid            baseTypeId;
+             int32        baseTypeMod;
              Oid            typrelid;
              AttrNumber    attnum;
              Oid            fieldTypeId;
*************** transformAssignmentIndirection(ParseStat
*** 752,758 ****

              /* No subscripts, so can process field selection here */

!             typrelid = typeidTypeRelid(targetTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
--- 754,767 ----

              /* No subscripts, so can process field selection here */

!             /*
!              * Look up the composite type, accounting for possibility that
!              * what we are given is a domain over composite.
!              */
!             baseTypeMod = targetTypMod;
!             baseTypeId = getBaseTypeAndTypmod(targetTypeId, &baseTypeMod);
!
!             typrelid = typeidTypeRelid(baseTypeId);
              if (!typrelid)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
*************** transformAssignmentIndirection(ParseStat
*** 796,802 ****
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = targetTypeId;

              return (Node *) fstore;
          }
--- 805,821 ----
              fstore->arg = (Expr *) basenode;
              fstore->newvals = list_make1(rhs);
              fstore->fieldnums = list_make1_int(attnum);
!             fstore->resulttype = baseTypeId;
!
!             /* If target is a domain, apply constraints */
!             if (baseTypeId != targetTypeId)
!                 return coerce_to_domain((Node *) fstore,
!                                         baseTypeId, baseTypeMod,
!                                         targetTypeId,
!                                         COERCION_IMPLICIT,
!                                         COERCE_IMPLICIT_CAST,
!                                         location,
!                                         false);

              return (Node *) fstore;
          }
*************** ExpandRowReference(ParseState *pstate, N
*** 1387,1408 ****
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.  We use
!      * get_expr_result_type() because that can handle references to functions
!      * returning anonymous record types.  If that fails, use
!      * lookup_rowtype_tupdesc(), which will almost certainly fail as well, but
!      * it will give an appropriate error message.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_type.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
--- 1406,1423 ----
       * (This can be pretty inefficient if the expression involves nontrivial
       * computation :-(.)
       *
!      * Verify it's a composite type, and get the tupdesc.
!      * get_expr_result_tupdesc() handles this conveniently.
       *
       * If it's a Var of type RECORD, we have to work even harder: we have to
!      * find what the Var refers to, and pass that to get_expr_result_tupdesc.
       * That task is handled by expandRecordVariable().
       */
      if (IsA(expr, Var) &&
          ((Var *) expr)->vartype == RECORDOID)
          tupleDesc = expandRecordVariable(pstate, (Var *) expr, 0);
!     else
!         tupleDesc = get_expr_result_tupdesc(expr, false);
      Assert(tupleDesc);

      /* Generate a list of references to the individual fields */
*************** expandRecordVariable(ParseState *pstate,
*** 1610,1624 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!
!     return tupleDesc;
  }


--- 1625,1633 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     return get_expr_result_tupdesc(expr, false);
  }


diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index d0b3fbe..b032651 100644
*** a/src/backend/parser/parse_type.c
--- b/src/backend/parser/parse_type.c
*************** stringTypeDatum(Type tp, char *string, i
*** 641,647 ****
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /* given a typeid, return the type's typrelid (associated relation, if any) */
  Oid
  typeidTypeRelid(Oid type_id)
  {
--- 641,650 ----
      return OidInputFunctionCall(typinput, string, typioparam, atttypmod);
  }

! /*
!  * Given a typeid, return the type's typrelid (associated relation), if any.
!  * Returns InvalidOid if type is not a composite type.
!  */
  Oid
  typeidTypeRelid(Oid type_id)
  {
*************** typeidTypeRelid(Oid type_id)
*** 652,658 ****
      typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
      if (!HeapTupleIsValid(typeTuple))
          elog(ERROR, "cache lookup failed for type %u", type_id);
-
      type = (Form_pg_type) GETSTRUCT(typeTuple);
      result = type->typrelid;
      ReleaseSysCache(typeTuple);
--- 655,660 ----
*************** typeidTypeRelid(Oid type_id)
*** 660,665 ****
--- 662,699 ----
  }

  /*
+  * Given a typeid, return the type's typrelid (associated relation), if any.
+  * Returns InvalidOid if type is not a composite type or a domain over one.
+  * This is the same as typeidTypeRelid(getBaseType(type_id)), but faster.
+  */
+ Oid
+ typeOrDomainTypeRelid(Oid type_id)
+ {
+     HeapTuple    typeTuple;
+     Form_pg_type type;
+     Oid            result;
+
+     for (;;)
+     {
+         typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_id));
+         if (!HeapTupleIsValid(typeTuple))
+             elog(ERROR, "cache lookup failed for type %u", type_id);
+         type = (Form_pg_type) GETSTRUCT(typeTuple);
+         if (type->typtype != TYPTYPE_DOMAIN)
+         {
+             /* Not a domain, so done looking through domains */
+             break;
+         }
+         /* It is a domain, so examine the base type instead */
+         type_id = type->typbasetype;
+         ReleaseSysCache(typeTuple);
+     }
+     result = type->typrelid;
+     ReleaseSysCache(typeTuple);
+     return result;
+ }
+
+ /*
   * error context callback for parse failure during parseTypeString()
   */
  static void
diff --git a/src/backend/utils/adt/domains.c b/src/backend/utils/adt/domains.c
index e61d91b..86f916f 100644
*** a/src/backend/utils/adt/domains.c
--- b/src/backend/utils/adt/domains.c
*************** domain_state_setup(Oid domainType, bool
*** 82,90 ****
       * Verify that domainType represents a valid domain type.  We need to be
       * careful here because domain_in and domain_recv can be called from SQL,
       * possibly with incorrect arguments.  We use lookup_type_cache mainly
!      * because it will throw a clean user-facing error for a bad OID.
       */
!     typentry = lookup_type_cache(domainType, 0);
      if (typentry->typtype != TYPTYPE_DOMAIN)
          ereport(ERROR,
                  (errcode(ERRCODE_DATATYPE_MISMATCH),
--- 82,91 ----
       * Verify that domainType represents a valid domain type.  We need to be
       * careful here because domain_in and domain_recv can be called from SQL,
       * possibly with incorrect arguments.  We use lookup_type_cache mainly
!      * because it will throw a clean user-facing error for a bad OID; but also
!      * it can cache the underlying base type info.
       */
!     typentry = lookup_type_cache(domainType, TYPECACHE_DOMAIN_BASE_INFO);
      if (typentry->typtype != TYPTYPE_DOMAIN)
          ereport(ERROR,
                  (errcode(ERRCODE_DATATYPE_MISMATCH),
*************** domain_state_setup(Oid domainType, bool
*** 92,99 ****
                          format_type_be(domainType))));

      /* Find out the base type */
!     my_extra->typtypmod = -1;
!     baseType = getBaseTypeAndTypmod(domainType, &my_extra->typtypmod);

      /* Look up underlying I/O function */
      if (binary)
--- 93,100 ----
                          format_type_be(domainType))));

      /* Find out the base type */
!     baseType = typentry->domainBaseType;
!     my_extra->typtypmod = typentry->domainBaseTypmod;

      /* Look up underlying I/O function */
      if (binary)
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index d36fd9e..76ef01c 100644
*** a/src/backend/utils/adt/jsonfuncs.c
--- b/src/backend/utils/adt/jsonfuncs.c
*************** typedef struct CompositeIOData
*** 169,174 ****
--- 169,179 ----
       */
      RecordIOData *record_io;    /* metadata cache for populate_record() */
      TupleDesc    tupdesc;        /* cached tuple descriptor */
+     /* these fields differ from target type only if domain over composite: */
+     Oid            base_typid;        /* base type id */
+     int32        base_typmod;    /* base type modifier */
+     /* this field is used only if target type is domain over composite: */
+     void       *domain_info;    /* opaque cache for domain checks */
  } CompositeIOData;

  /* structure to cache metadata needed for populate_domain() */
*************** typedef enum TypeCat
*** 186,191 ****
--- 191,197 ----
      TYPECAT_SCALAR = 's',
      TYPECAT_ARRAY = 'a',
      TYPECAT_COMPOSITE = 'c',
+     TYPECAT_COMPOSITE_DOMAIN = 'C',
      TYPECAT_DOMAIN = 'd'
  } TypeCat;

*************** struct RecordIOData
*** 217,223 ****
      ColumnIOData columns[FLEXIBLE_ARRAY_MEMBER];
  };

! /* state for populate_recordset */
  typedef struct PopulateRecordsetState
  {
      JsonLexContext *lex;
--- 223,237 ----
      ColumnIOData columns[FLEXIBLE_ARRAY_MEMBER];
  };

! /* per-query cache for populate_recordset */
! typedef struct PopulateRecordsetCache
! {
!     Oid            argtype;        /* declared type of the record argument */
!     ColumnIOData c;                /* metadata cache for populate_composite() */
!     MemoryContext fn_mcxt;        /* where this is stored */
! } PopulateRecordsetCache;
!
! /* per-call state for populate_recordset */
  typedef struct PopulateRecordsetState
  {
      JsonLexContext *lex;
*************** typedef struct PopulateRecordsetState
*** 227,243 ****
      char       *save_json_start;
      JsonTokenType saved_token_type;
      Tuplestorestate *tuple_store;
-     TupleDesc    ret_tdesc;
      HeapTupleHeader rec;
!     RecordIOData **my_extra;
!     MemoryContext fn_mcxt;        /* used to stash IO funcs */
  } PopulateRecordsetState;

  /* structure to cache metadata needed for populate_record_worker() */
  typedef struct PopulateRecordCache
  {
!     Oid            argtype;        /* verified row type of the first argument */
!     CompositeIOData io;            /* metadata cache for populate_composite() */
  } PopulateRecordCache;

  /* common data for populate_array_json() and populate_array_dim_jsonb() */
--- 241,255 ----
      char       *save_json_start;
      JsonTokenType saved_token_type;
      Tuplestorestate *tuple_store;
      HeapTupleHeader rec;
!     PopulateRecordsetCache *cache;
  } PopulateRecordsetState;

  /* structure to cache metadata needed for populate_record_worker() */
  typedef struct PopulateRecordCache
  {
!     Oid            argtype;        /* declared type of the record argument */
!     ColumnIOData c;                /* metadata cache for populate_composite() */
  } PopulateRecordCache;

  /* common data for populate_array_json() and populate_array_dim_jsonb() */
*************** static Datum populate_record_worker(Func
*** 415,430 ****
  static HeapTupleHeader populate_record(TupleDesc tupdesc, RecordIOData **record_p,
                  HeapTupleHeader defaultval, MemoryContext mcxt,
                  JsObject *obj);
- static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod,
-                       const char *colname, MemoryContext mcxt,
-                       Datum defaultval, JsValue *jsv, bool *isnull);
  static void JsValueToJsObject(JsValue *jsv, JsObject *jso);
! static Datum populate_composite(CompositeIOData *io, Oid typid, int32 typmod,
                     const char *colname, MemoryContext mcxt,
!                    HeapTupleHeader defaultval, JsValue *jsv);
  static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv);
  static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod,
!                      MemoryContext mcxt, bool json);
  static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod,
                        const char *colname, MemoryContext mcxt, Datum defaultval,
                        JsValue *jsv, bool *isnull);
--- 427,439 ----
  static HeapTupleHeader populate_record(TupleDesc tupdesc, RecordIOData **record_p,
                  HeapTupleHeader defaultval, MemoryContext mcxt,
                  JsObject *obj);
  static void JsValueToJsObject(JsValue *jsv, JsObject *jso);
! static Datum populate_composite(CompositeIOData *io, Oid typid,
                     const char *colname, MemoryContext mcxt,
!                    HeapTupleHeader defaultval, JsValue *jsv, bool isnull);
  static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv);
  static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod,
!                      MemoryContext mcxt, bool need_scalar);
  static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod,
                        const char *colname, MemoryContext mcxt, Datum defaultval,
                        JsValue *jsv, bool *isnull);
*************** JsValueToJsObject(JsValue *jsv, JsObject
*** 2704,2728 ****
      }
  }

! /* recursively populate a composite (row type) value from json/jsonb */
! static Datum
! populate_composite(CompositeIOData *io,
!                    Oid typid,
!                    int32 typmod,
!                    const char *colname,
!                    MemoryContext mcxt,
!                    HeapTupleHeader defaultval,
!                    JsValue *jsv)
  {
-     HeapTupleHeader tuple;
-     JsObject    jso;
-
-     /* acquire cached tuple descriptor */
      if (!io->tupdesc ||
!         io->tupdesc->tdtypeid != typid ||
!         io->tupdesc->tdtypmod != typmod)
      {
!         TupleDesc    tupdesc = lookup_rowtype_tupdesc(typid, typmod);
          MemoryContext oldcxt;

          if (io->tupdesc)
--- 2713,2728 ----
      }
  }

! /* acquire or update cached tuple descriptor for a composite type */
! static void
! update_cached_tupdesc(CompositeIOData *io, MemoryContext mcxt)
  {
      if (!io->tupdesc ||
!         io->tupdesc->tdtypeid != io->base_typid ||
!         io->tupdesc->tdtypmod != io->base_typmod)
      {
!         TupleDesc    tupdesc = lookup_rowtype_tupdesc(io->base_typid,
!                                                      io->base_typmod);
          MemoryContext oldcxt;

          if (io->tupdesc)
*************** populate_composite(CompositeIOData *io,
*** 2735,2751 ****

          ReleaseTupleDesc(tupdesc);
      }

!     /* prepare input value */
!     JsValueToJsObject(jsv, &jso);

!     /* populate resulting record tuple */
!     tuple = populate_record(io->tupdesc, &io->record_io,
!                             defaultval, mcxt, &jso);

!     JsObjectFree(&jso);

!     return HeapTupleHeaderGetDatum(tuple);
  }

  /* populate non-null scalar value from json/jsonb value */
--- 2735,2780 ----

          ReleaseTupleDesc(tupdesc);
      }
+ }

! /* recursively populate a composite (row type) value from json/jsonb */
! static Datum
! populate_composite(CompositeIOData *io,
!                    Oid typid,
!                    const char *colname,
!                    MemoryContext mcxt,
!                    HeapTupleHeader defaultval,
!                    JsValue *jsv,
!                    bool isnull)
! {
!     Datum        result;

!     /* acquire/update cached tuple descriptor */
!     update_cached_tupdesc(io, mcxt);

!     if (isnull)
!         result = (Datum) 0;
!     else
!     {
!         HeapTupleHeader tuple;
!         JsObject    jso;

!         /* prepare input value */
!         JsValueToJsObject(jsv, &jso);
!
!         /* populate resulting record tuple */
!         tuple = populate_record(io->tupdesc, &io->record_io,
!                                 defaultval, mcxt, &jso);
!         result = HeapTupleHeaderGetDatum(tuple);
!
!         JsObjectFree(&jso);
!     }
!
!     /* if it's domain over composite, check domain constraints */
!     if (typid != io->base_typid)
!         domain_check(result, isnull, typid, &io->domain_info, mcxt);
!
!     return result;
  }

  /* populate non-null scalar value from json/jsonb value */
*************** prepare_column_cache(ColumnIOData *colum
*** 2867,2873 ****
                       Oid typid,
                       int32 typmod,
                       MemoryContext mcxt,
!                      bool json)
  {
      HeapTuple    tup;
      Form_pg_type type;
--- 2896,2902 ----
                       Oid typid,
                       int32 typmod,
                       MemoryContext mcxt,
!                      bool need_scalar)
  {
      HeapTuple    tup;
      Form_pg_type type;
*************** prepare_column_cache(ColumnIOData *colum
*** 2883,2900 ****

      if (type->typtype == TYPTYPE_DOMAIN)
      {
!         column->typcat = TYPECAT_DOMAIN;
!         column->io.domain.base_typid = type->typbasetype;
!         column->io.domain.base_typmod = type->typtypmod;
!         column->io.domain.base_io = MemoryContextAllocZero(mcxt,
!                                                            sizeof(ColumnIOData));
!         column->io.domain.domain_info = NULL;
      }
      else if (type->typtype == TYPTYPE_COMPOSITE || typid == RECORDOID)
      {
          column->typcat = TYPECAT_COMPOSITE;
          column->io.composite.record_io = NULL;
          column->io.composite.tupdesc = NULL;
      }
      else if (type->typlen == -1 && OidIsValid(type->typelem))
      {
--- 2912,2954 ----

      if (type->typtype == TYPTYPE_DOMAIN)
      {
!         /*
!          * We can move directly to the bottom base type; domain_check() will
!          * take care of checking all constraints for a stack of domains.
!          */
!         Oid            base_typid;
!         int32        base_typmod = typmod;
!
!         base_typid = getBaseTypeAndTypmod(typid, &base_typmod);
!         if (get_typtype(base_typid) == TYPTYPE_COMPOSITE)
!         {
!             /* domain over composite has its own code path */
!             column->typcat = TYPECAT_COMPOSITE_DOMAIN;
!             column->io.composite.record_io = NULL;
!             column->io.composite.tupdesc = NULL;
!             column->io.composite.base_typid = base_typid;
!             column->io.composite.base_typmod = base_typmod;
!             column->io.composite.domain_info = NULL;
!         }
!         else
!         {
!             /* domain over anything else */
!             column->typcat = TYPECAT_DOMAIN;
!             column->io.domain.base_typid = base_typid;
!             column->io.domain.base_typmod = base_typmod;
!             column->io.domain.base_io =
!                 MemoryContextAllocZero(mcxt, sizeof(ColumnIOData));
!             column->io.domain.domain_info = NULL;
!         }
      }
      else if (type->typtype == TYPTYPE_COMPOSITE || typid == RECORDOID)
      {
          column->typcat = TYPECAT_COMPOSITE;
          column->io.composite.record_io = NULL;
          column->io.composite.tupdesc = NULL;
+         column->io.composite.base_typid = typid;
+         column->io.composite.base_typmod = typmod;
+         column->io.composite.domain_info = NULL;
      }
      else if (type->typlen == -1 && OidIsValid(type->typelem))
      {
*************** prepare_column_cache(ColumnIOData *colum
*** 2906,2915 ****
          column->io.array.element_typmod = typmod;
      }
      else
          column->typcat = TYPECAT_SCALAR;

!     /* don't need input function when converting from jsonb to jsonb */
!     if (json || typid != JSONBOID)
      {
          Oid            typioproc;

--- 2960,2972 ----
          column->io.array.element_typmod = typmod;
      }
      else
+     {
          column->typcat = TYPECAT_SCALAR;
+         need_scalar = true;
+     }

!     /* caller can force us to look up scalar_io info even for non-scalars */
!     if (need_scalar)
      {
          Oid            typioproc;

*************** populate_record_field(ColumnIOData *col,
*** 2935,2943 ****

      check_stack_depth();

!     /* prepare column metadata cache for the given type */
      if (col->typid != typid || col->typmod != typmod)
!         prepare_column_cache(col, typid, typmod, mcxt, jsv->is_json);

      *isnull = JsValueIsNull(jsv);

--- 2992,3003 ----

      check_stack_depth();

!     /*
!      * Prepare column metadata cache for the given type.  Force lookup of the
!      * scalar_io data so that the json string hack below will work.
!      */
      if (col->typid != typid || col->typmod != typmod)
!         prepare_column_cache(col, typid, typmod, mcxt, true);

      *isnull = JsValueIsNull(jsv);

*************** populate_record_field(ColumnIOData *col,
*** 2945,2955 ****

      /* try to convert json string to a non-scalar type through input function */
      if (JsValueIsString(jsv) &&
!         (typcat == TYPECAT_ARRAY || typcat == TYPECAT_COMPOSITE))
          typcat = TYPECAT_SCALAR;

!     /* we must perform domain checks for NULLs */
!     if (*isnull && typcat != TYPECAT_DOMAIN)
          return (Datum) 0;

      switch (typcat)
--- 3005,3019 ----

      /* try to convert json string to a non-scalar type through input function */
      if (JsValueIsString(jsv) &&
!         (typcat == TYPECAT_ARRAY ||
!          typcat == TYPECAT_COMPOSITE ||
!          typcat == TYPECAT_COMPOSITE_DOMAIN))
          typcat = TYPECAT_SCALAR;

!     /* we must perform domain checks for NULLs, otherwise exit immediately */
!     if (*isnull &&
!         typcat != TYPECAT_DOMAIN &&
!         typcat != TYPECAT_COMPOSITE_DOMAIN)
          return (Datum) 0;

      switch (typcat)
*************** populate_record_field(ColumnIOData *col,
*** 2961,2972 ****
              return populate_array(&col->io.array, colname, mcxt, jsv);

          case TYPECAT_COMPOSITE:
!             return populate_composite(&col->io.composite, typid, typmod,
                                        colname, mcxt,
                                        DatumGetPointer(defaultval)
                                        ? DatumGetHeapTupleHeader(defaultval)
                                        : NULL,
!                                       jsv);

          case TYPECAT_DOMAIN:
              return populate_domain(&col->io.domain, typid, colname, mcxt,
--- 3025,3037 ----
              return populate_array(&col->io.array, colname, mcxt, jsv);

          case TYPECAT_COMPOSITE:
!         case TYPECAT_COMPOSITE_DOMAIN:
!             return populate_composite(&col->io.composite, typid,
                                        colname, mcxt,
                                        DatumGetPointer(defaultval)
                                        ? DatumGetHeapTupleHeader(defaultval)
                                        : NULL,
!                                       jsv, *isnull);

          case TYPECAT_DOMAIN:
              return populate_domain(&col->io.domain, typid, colname, mcxt,
*************** populate_record_worker(FunctionCallInfo
*** 3137,3146 ****
      int            json_arg_num = have_record_arg ? 1 : 0;
      Oid            jtype = get_fn_expr_argtype(fcinfo->flinfo, json_arg_num);
      JsValue        jsv = {0};
!     HeapTupleHeader rec = NULL;
!     Oid            tupType;
!     int32        tupTypmod;
!     TupleDesc    tupdesc = NULL;
      Datum        rettuple;
      JsonbValue    jbv;
      MemoryContext fnmcxt = fcinfo->flinfo->fn_mcxt;
--- 3202,3208 ----
      int            json_arg_num = have_record_arg ? 1 : 0;
      Oid            jtype = get_fn_expr_argtype(fcinfo->flinfo, json_arg_num);
      JsValue        jsv = {0};
!     HeapTupleHeader rec;
      Datum        rettuple;
      JsonbValue    jbv;
      MemoryContext fnmcxt = fcinfo->flinfo->fn_mcxt;
*************** populate_record_worker(FunctionCallInfo
*** 3149,3225 ****
      Assert(jtype == JSONOID || jtype == JSONBOID);

      /*
!      * We arrange to look up the needed I/O info just once per series of
!      * calls, assuming the record type doesn't change underneath us.
       */
      if (!cache)
          fcinfo->flinfo->fn_extra = cache =
              MemoryContextAllocZero(fnmcxt, sizeof(*cache));

!     if (have_record_arg)
!     {
!         Oid            argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);
!
!         if (cache->argtype != argtype)
          {
!             if (!type_is_rowtype(argtype))
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
                           errmsg("first argument of %s must be a row type",
                                  funcname)));
-
-             cache->argtype = argtype;
          }
!
!         if (PG_ARGISNULL(0))
          {
-             if (PG_ARGISNULL(1))
-                 PG_RETURN_NULL();
-
              /*
!              * We have no tuple to look at, so the only source of type info is
!              * the argtype. The lookup_rowtype_tupdesc call below will error
!              * out if we don't have a known composite type oid here.
               */
!             tupType = argtype;
!             tupTypmod = -1;
!         }
!         else
!         {
!             rec = PG_GETARG_HEAPTUPLEHEADER(0);

!             if (PG_ARGISNULL(1))
!                 PG_RETURN_POINTER(rec);

!             /* Extract type info from the tuple itself */
!             tupType = HeapTupleHeaderGetTypeId(rec);
!             tupTypmod = HeapTupleHeaderGetTypMod(rec);
          }
      }
-     else
-     {
-         /* json{b}_to_record case */
-         if (PG_ARGISNULL(0))
-             PG_RETURN_NULL();
-
-         if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-             ereport(ERROR,
-                     (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                      errmsg("function returning record called in context "
-                             "that cannot accept type record"),
-                      errhint("Try calling the function in the FROM clause "
-                              "using a column definition list.")));

!         Assert(tupdesc);

          /*
!          * Add tupdesc to the cache and set the appropriate values of
!          * tupType/tupTypmod for proper cache usage in populate_composite().
           */
!         cache->io.tupdesc = tupdesc;

!         tupType = tupdesc->tdtypeid;
!         tupTypmod = tupdesc->tdtypmod;
      }

      jsv.is_json = jtype == JSONOID;
--- 3211,3295 ----
      Assert(jtype == JSONOID || jtype == JSONBOID);

      /*
!      * If first time through, identify input/result record type.  Note that
!      * this stanza looks only at fcinfo context, which can't change during the
!      * query; so we may not be able to fully resolve a RECORD input type yet.
       */
      if (!cache)
+     {
          fcinfo->flinfo->fn_extra = cache =
              MemoryContextAllocZero(fnmcxt, sizeof(*cache));

!         if (have_record_arg)
          {
!             /*
!              * json{b}_populate_record case: result type will be same as first
!              * argument's.
!              */
!             cache->argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);
!             prepare_column_cache(&cache->c,
!                                  cache->argtype, -1,
!                                  fnmcxt, false);
!             if (cache->c.typcat != TYPECAT_COMPOSITE &&
!                 cache->c.typcat != TYPECAT_COMPOSITE_DOMAIN)
                  ereport(ERROR,
                          (errcode(ERRCODE_DATATYPE_MISMATCH),
                           errmsg("first argument of %s must be a row type",
                                  funcname)));
          }
!         else
          {
              /*
!              * json{b}_to_record case: result type is specified by calling
!              * query.  Here it is syntactically impossible to specify the
!              * target type as domain-over-composite.
               */
!             TupleDesc    tupdesc;
!             MemoryContext old_cxt;

!             if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
!                 ereport(ERROR,
!                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                          errmsg("function returning record called in context "
!                                 "that cannot accept type record"),
!                          errhint("Try calling the function in the FROM clause "
!                                  "using a column definition list.")));

!             Assert(tupdesc);
!             cache->argtype = tupdesc->tdtypeid;
!
!             /* Save identified tupdesc */
!             old_cxt = MemoryContextSwitchTo(fnmcxt);
!             cache->c.io.composite.tupdesc = CreateTupleDescCopy(tupdesc);
!             cache->c.io.composite.base_typid = tupdesc->tdtypeid;
!             cache->c.io.composite.base_typmod = tupdesc->tdtypmod;
!             MemoryContextSwitchTo(old_cxt);
          }
      }

!     /* Collect record arg if we have one */
!     if (have_record_arg && !PG_ARGISNULL(0))
!     {
!         rec = PG_GETARG_HEAPTUPLEHEADER(0);

          /*
!          * When declared arg type is RECORD, identify actual record type from
!          * the tuple itself.  Note the lookup_rowtype_tupdesc call in
!          * update_cached_tupdesc will fail if we're unable to do this.
           */
!         if (cache->argtype == RECORDOID)
!             cache->c.io.composite.base_typmod = HeapTupleHeaderGetTypMod(rec);
!     }
!     else
!         rec = NULL;

!     /* If no JSON argument, just return the record (if any) unchanged */
!     if (PG_ARGISNULL(json_arg_num))
!     {
!         if (rec)
!             PG_RETURN_POINTER(rec);
!         else
!             PG_RETURN_NULL();
      }

      jsv.is_json = jtype == JSONOID;
*************** populate_record_worker(FunctionCallInfo
*** 3245,3258 ****
          jbv.val.binary.len = VARSIZE(jb) - VARHDRSZ;
      }

!     rettuple = populate_composite(&cache->io, tupType, tupTypmod,
!                                   NULL, fnmcxt, rec, &jsv);
!
!     if (tupdesc)
!     {
!         cache->io.tupdesc = NULL;
!         ReleaseTupleDesc(tupdesc);
!     }

      PG_RETURN_DATUM(rettuple);
  }
--- 3315,3322 ----
          jbv.val.binary.len = VARSIZE(jb) - VARHDRSZ;
      }

!     rettuple = populate_composite(&cache->c.io.composite, cache->argtype,
!                                   NULL, fnmcxt, rec, &jsv, false);

      PG_RETURN_DATUM(rettuple);
  }
*************** json_to_recordset(PG_FUNCTION_ARGS)
*** 3438,3450 ****
  static void
  populate_recordset_record(PopulateRecordsetState *state, JsObject *obj)
  {
      HeapTupleData tuple;
-     HeapTupleHeader tuphead = populate_record(state->ret_tdesc,
-                                               state->my_extra,
-                                               state->rec,
-                                               state->fn_mcxt,
-                                               obj);

      tuple.t_len = HeapTupleHeaderGetDatumLength(tuphead);
      ItemPointerSetInvalid(&(tuple.t_self));
      tuple.t_tableOid = InvalidOid;
--- 3502,3529 ----
  static void
  populate_recordset_record(PopulateRecordsetState *state, JsObject *obj)
  {
+     PopulateRecordsetCache *cache = state->cache;
+     HeapTupleHeader tuphead;
      HeapTupleData tuple;

+     /* acquire/update cached tuple descriptor */
+     update_cached_tupdesc(&cache->c.io.composite, cache->fn_mcxt);
+
+     /* replace record fields from json */
+     tuphead = populate_record(cache->c.io.composite.tupdesc,
+                               &cache->c.io.composite.record_io,
+                               state->rec,
+                               cache->fn_mcxt,
+                               obj);
+
+     /* if it's domain over composite, check domain constraints */
+     if (cache->argtype != cache->c.io.composite.base_typid)
+         domain_check(HeapTupleHeaderGetDatum(tuphead), false,
+                      cache->argtype,
+                      &cache->c.io.composite.domain_info,
+                      cache->fn_mcxt);
+
+     /* ok, save into tuplestore */
      tuple.t_len = HeapTupleHeaderGetDatumLength(tuphead);
      ItemPointerSetInvalid(&(tuple.t_self));
      tuple.t_tableOid = InvalidOid;
*************** populate_recordset_worker(FunctionCallIn
*** 3465,3489 ****
      ReturnSetInfo *rsi;
      MemoryContext old_cxt;
      HeapTupleHeader rec;
!     TupleDesc    tupdesc;
      PopulateRecordsetState *state;

-     if (have_record_arg)
-     {
-         Oid            argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);
-
-         if (!type_is_rowtype(argtype))
-             ereport(ERROR,
-                     (errcode(ERRCODE_DATATYPE_MISMATCH),
-                      errmsg("first argument of %s must be a row type",
-                             funcname)));
-     }
-
      rsi = (ReturnSetInfo *) fcinfo->resultinfo;

      if (!rsi || !IsA(rsi, ReturnSetInfo) ||
!         (rsi->allowedModes & SFRM_Materialize) == 0 ||
!         rsi->expectedDesc == NULL)
          ereport(ERROR,
                  (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                   errmsg("set-valued function called in context that "
--- 3544,3556 ----
      ReturnSetInfo *rsi;
      MemoryContext old_cxt;
      HeapTupleHeader rec;
!     PopulateRecordsetCache *cache = fcinfo->flinfo->fn_extra;
      PopulateRecordsetState *state;

      rsi = (ReturnSetInfo *) fcinfo->resultinfo;

      if (!rsi || !IsA(rsi, ReturnSetInfo) ||
!         (rsi->allowedModes & SFRM_Materialize) == 0)
          ereport(ERROR,
                  (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                   errmsg("set-valued function called in context that "
*************** populate_recordset_worker(FunctionCallIn
*** 3492,3531 ****
      rsi->returnMode = SFRM_Materialize;

      /*
!      * get the tupdesc from the result set info - it must be a record type
!      * because we already checked that arg1 is a record type, or we're in a
!      * to_record function which returns a setof record.
       */
!     if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
!         ereport(ERROR,
!                 (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                  errmsg("function returning record called in context "
!                         "that cannot accept type record")));

      /* if the json is null send back an empty set */
      if (PG_ARGISNULL(json_arg_num))
          PG_RETURN_NULL();

-     if (!have_record_arg || PG_ARGISNULL(0))
-         rec = NULL;
-     else
-         rec = PG_GETARG_HEAPTUPLEHEADER(0);
-
      state = palloc0(sizeof(PopulateRecordsetState));

!     /* make these in a sufficiently long-lived memory context */
      old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
-     state->ret_tdesc = CreateTupleDescCopy(tupdesc);
-     BlessTupleDesc(state->ret_tdesc);
      state->tuple_store = tuplestore_begin_heap(rsi->allowedModes &
                                                 SFRM_Materialize_Random,
                                                 false, work_mem);
      MemoryContextSwitchTo(old_cxt);

      state->function_name = funcname;
!     state->my_extra = (RecordIOData **) &fcinfo->flinfo->fn_extra;
      state->rec = rec;
-     state->fn_mcxt = fcinfo->flinfo->fn_mcxt;

      if (jtype == JSONOID)
      {
--- 3559,3652 ----
      rsi->returnMode = SFRM_Materialize;

      /*
!      * If first time through, identify input/result record type.  Note that
!      * this stanza looks only at fcinfo context, which can't change during the
!      * query; so we may not be able to fully resolve a RECORD input type yet.
       */
!     if (!cache)
!     {
!         fcinfo->flinfo->fn_extra = cache =
!             MemoryContextAllocZero(fcinfo->flinfo->fn_mcxt, sizeof(*cache));
!         cache->fn_mcxt = fcinfo->flinfo->fn_mcxt;
!
!         if (have_record_arg)
!         {
!             /*
!              * json{b}_populate_recordset case: result type will be same as
!              * first argument's.
!              */
!             cache->argtype = get_fn_expr_argtype(fcinfo->flinfo, 0);
!             prepare_column_cache(&cache->c,
!                                  cache->argtype, -1,
!                                  cache->fn_mcxt, false);
!             if (cache->c.typcat != TYPECAT_COMPOSITE &&
!                 cache->c.typcat != TYPECAT_COMPOSITE_DOMAIN)
!                 ereport(ERROR,
!                         (errcode(ERRCODE_DATATYPE_MISMATCH),
!                          errmsg("first argument of %s must be a row type",
!                                 funcname)));
!         }
!         else
!         {
!             /*
!              * json{b}_to_recordset case: result type is specified by calling
!              * query.  Here it is syntactically impossible to specify the
!              * target type as domain-over-composite.
!              */
!             TupleDesc    tupdesc;
!
!             if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
!                 ereport(ERROR,
!                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                          errmsg("function returning record called in context "
!                                 "that cannot accept type record"),
!                          errhint("Try calling the function in the FROM clause "
!                                  "using a column definition list.")));
!
!             Assert(tupdesc);
!             cache->argtype = tupdesc->tdtypeid;
!
!             /* Save identified tupdesc */
!             old_cxt = MemoryContextSwitchTo(cache->fn_mcxt);
!             cache->c.io.composite.tupdesc = CreateTupleDescCopy(tupdesc);
!             cache->c.io.composite.base_typid = tupdesc->tdtypeid;
!             cache->c.io.composite.base_typmod = tupdesc->tdtypmod;
!             MemoryContextSwitchTo(old_cxt);
!         }
!     }
!
!     /* Collect record arg if we have one */
!     if (have_record_arg && !PG_ARGISNULL(0))
!     {
!         rec = PG_GETARG_HEAPTUPLEHEADER(0);
!
!         /*
!          * When declared arg type is RECORD, identify actual record type from
!          * the tuple itself.  Note the lookup_rowtype_tupdesc call in
!          * update_cached_tupdesc will fail if we're unable to do this.
!          */
!         if (cache->argtype == RECORDOID)
!             cache->c.io.composite.base_typmod = HeapTupleHeaderGetTypMod(rec);
!     }
!     else
!         rec = NULL;

      /* if the json is null send back an empty set */
      if (PG_ARGISNULL(json_arg_num))
          PG_RETURN_NULL();

      state = palloc0(sizeof(PopulateRecordsetState));

!     /* make tuplestore in a sufficiently long-lived memory context */
      old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
      state->tuple_store = tuplestore_begin_heap(rsi->allowedModes &
                                                 SFRM_Materialize_Random,
                                                 false, work_mem);
      MemoryContextSwitchTo(old_cxt);

      state->function_name = funcname;
!     state->cache = cache;
      state->rec = rec;

      if (jtype == JSONOID)
      {
*************** populate_recordset_worker(FunctionCallIn
*** 3592,3598 ****
      }

      rsi->setResult = state->tuple_store;
!     rsi->setDesc = state->ret_tdesc;

      PG_RETURN_NULL();
  }
--- 3713,3719 ----
      }

      rsi->setResult = state->tuple_store;
!     rsi->setDesc = cache->c.io.composite.tupdesc;

      PG_RETURN_NULL();
  }
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 84759b6..b1e70a0 100644
*** a/src/backend/utils/adt/ruleutils.c
--- b/src/backend/utils/adt/ruleutils.c
*************** get_name_for_var_field(Var *var, int fie
*** 6731,6747 ****

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_type. If that fails, we try
!      * lookup_rowtype_tupdesc, which will probably fail too, but will ereport
!      * an acceptable message.
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         if (get_expr_result_type((Node *) var, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!             tupleDesc = lookup_rowtype_tupdesc_copy(exprType((Node *) var),
!                                                     exprTypmod((Node *) var));
!         Assert(tupleDesc);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
--- 6731,6742 ----

      /*
       * If it's a Var of type RECORD, we have to find what the Var refers to;
!      * if not, we can use get_expr_result_tupdesc().
       */
      if (!IsA(var, Var) ||
          var->vartype != RECORDOID)
      {
!         tupleDesc = get_expr_result_tupdesc((Node *) var, false);
          /* Got the tupdesc, so we can extract the field name */
          Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
          return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
*************** get_name_for_var_field(Var *var, int fie
*** 7044,7057 ****

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_type() can do anything with it.  If not, pass to
!      * lookup_rowtype_tupdesc() which will probably fail, but will give an
!      * appropriate error message while failing.
       */
!     if (get_expr_result_type(expr, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
!         tupleDesc = lookup_rowtype_tupdesc_copy(exprType(expr),
!                                                 exprTypmod(expr));
!     Assert(tupleDesc);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
--- 7039,7047 ----

      /*
       * We now have an expression we can't expand any more, so see if
!      * get_expr_result_tupdesc() can do anything with it.
       */
!     tupleDesc = get_expr_result_tupdesc(expr, false);
      /* Got the tupdesc, so we can extract the field name */
      Assert(fieldno >= 1 && fieldno <= tupleDesc->natts);
      return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index b7a14dc..48961e3 100644
*** a/src/backend/utils/cache/lsyscache.c
--- b/src/backend/utils/cache/lsyscache.c
*************** get_typtype(Oid typid)
*** 2398,2409 ****
   * type_is_rowtype
   *
   *        Convenience function to determine whether a type OID represents
!  *        a "rowtype" type --- either RECORD or a named composite type.
   */
  bool
  type_is_rowtype(Oid typid)
  {
!     return (typid == RECORDOID || get_typtype(typid) == TYPTYPE_COMPOSITE);
  }

  /*
--- 2398,2423 ----
   * type_is_rowtype
   *
   *        Convenience function to determine whether a type OID represents
!  *        a "rowtype" type --- either RECORD or a named composite type
!  *        (including a domain over a named composite type).
   */
  bool
  type_is_rowtype(Oid typid)
  {
!     if (typid == RECORDOID)
!         return true;            /* easy case */
!     switch (get_typtype(typid))
!     {
!         case TYPTYPE_COMPOSITE:
!             return true;
!         case TYPTYPE_DOMAIN:
!             if (get_typtype(getBaseType(typid)) == TYPTYPE_COMPOSITE)
!                 return true;
!             break;
!         default:
!             break;
!     }
!     return false;
  }

  /*
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 61ce7dc..7aadc5d 100644
*** a/src/backend/utils/cache/typcache.c
--- b/src/backend/utils/cache/typcache.c
*************** static TypeCacheEntry *firstDomainTypeEn
*** 96,101 ****
--- 96,102 ----
  #define TCFLAGS_HAVE_FIELD_EQUALITY            0x004000
  #define TCFLAGS_HAVE_FIELD_COMPARE            0x008000
  #define TCFLAGS_CHECKED_DOMAIN_CONSTRAINTS    0x010000
+ #define TCFLAGS_DOMAIN_BASE_IS_COMPOSITE    0x020000

  /*
   * Data stored about a domain type's constraints.  Note that we do not create
*************** lookup_type_cache(Oid type_id, int flags
*** 747,753 ****
      /*
       * If requested, get information about a domain type
       */
!     if ((flags & TYPECACHE_DOMAIN_INFO) &&
          (typentry->flags & TCFLAGS_CHECKED_DOMAIN_CONSTRAINTS) == 0 &&
          typentry->typtype == TYPTYPE_DOMAIN)
      {
--- 748,762 ----
      /*
       * If requested, get information about a domain type
       */
!     if ((flags & TYPECACHE_DOMAIN_BASE_INFO) &&
!         typentry->domainBaseType == InvalidOid &&
!         typentry->typtype == TYPTYPE_DOMAIN)
!     {
!         typentry->domainBaseTypmod = -1;
!         typentry->domainBaseType =
!             getBaseTypeAndTypmod(type_id, &typentry->domainBaseTypmod);
!     }
!     if ((flags & TYPECACHE_DOMAIN_CONSTR_INFO) &&
          (typentry->flags & TCFLAGS_CHECKED_DOMAIN_CONSTRAINTS) == 0 &&
          typentry->typtype == TYPTYPE_DOMAIN)
      {
*************** InitDomainConstraintRef(Oid type_id, Dom
*** 1166,1172 ****
                          MemoryContext refctx, bool need_exprstate)
  {
      /* Look up the typcache entry --- we assume it survives indefinitely */
!     ref->tcache = lookup_type_cache(type_id, TYPECACHE_DOMAIN_INFO);
      ref->need_exprstate = need_exprstate;
      /* For safety, establish the callback before acquiring a refcount */
      ref->refctx = refctx;
--- 1175,1181 ----
                          MemoryContext refctx, bool need_exprstate)
  {
      /* Look up the typcache entry --- we assume it survives indefinitely */
!     ref->tcache = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
      ref->need_exprstate = need_exprstate;
      /* For safety, establish the callback before acquiring a refcount */
      ref->refctx = refctx;
*************** DomainHasConstraints(Oid type_id)
*** 1257,1263 ****
       * Note: a side effect is to cause the typcache's domain data to become
       * valid.  This is fine since we'll likely need it soon if there is any.
       */
!     typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_INFO);

      return (typentry->domainData != NULL);
  }
--- 1266,1272 ----
       * Note: a side effect is to cause the typcache's domain data to become
       * valid.  This is fine since we'll likely need it soon if there is any.
       */
!     typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);

      return (typentry->domainData != NULL);
  }
*************** cache_record_field_properties(TypeCacheE
*** 1405,1410 ****
--- 1414,1442 ----

          DecrTupleDescRefCount(tupdesc);
      }
+     else if (typentry->typtype == TYPTYPE_DOMAIN)
+     {
+         /* If it's domain over composite, copy base type's properties */
+         TypeCacheEntry *baseentry;
+
+         /* load up basetype info if we didn't already */
+         if (typentry->domainBaseType == InvalidOid)
+         {
+             typentry->domainBaseTypmod = -1;
+             typentry->domainBaseType =
+                 getBaseTypeAndTypmod(typentry->type_id,
+                                      &typentry->domainBaseTypmod);
+         }
+         baseentry = lookup_type_cache(typentry->domainBaseType,
+                                       TYPECACHE_EQ_OPR |
+                                       TYPECACHE_CMP_PROC);
+         if (baseentry->typtype == TYPTYPE_COMPOSITE)
+         {
+             typentry->flags |= TCFLAGS_DOMAIN_BASE_IS_COMPOSITE;
+             typentry->flags |= baseentry->flags & (TCFLAGS_HAVE_FIELD_EQUALITY |
+                                                    TCFLAGS_HAVE_FIELD_COMPARE);
+         }
+     }
      typentry->flags |= TCFLAGS_CHECKED_FIELD_PROPERTIES;
  }

*************** lookup_rowtype_tupdesc_copy(Oid type_id,
*** 1619,1624 ****
--- 1651,1703 ----
  }

  /*
+  * lookup_rowtype_tupdesc_domain
+  *
+  * Same as lookup_rowtype_tupdesc_noerror(), except that the type can also be
+  * a domain over a named composite type; so this is effectively equivalent to
+  * lookup_rowtype_tupdesc_noerror(getBaseType(type_id), typmod, noError)
+  * except for being a tad faster.
+  *
+  * Note: the reason we don't fold the look-through-domain behavior into plain
+  * lookup_rowtype_tupdesc() is that we want callers to know they might be
+  * dealing with a domain.  Otherwise they might construct a tuple that should
+  * be of the domain type, but not apply domain constraints.
+  */
+ TupleDesc
+ lookup_rowtype_tupdesc_domain(Oid type_id, int32 typmod, bool noError)
+ {
+     TupleDesc    tupDesc;
+
+     if (type_id != RECORDOID)
+     {
+         /*
+          * Check for domain or named composite type.  We might as well load
+          * whichever data is needed.
+          */
+         TypeCacheEntry *typentry;
+
+         typentry = lookup_type_cache(type_id,
+                                      TYPECACHE_TUPDESC |
+                                      TYPECACHE_DOMAIN_BASE_INFO);
+         if (typentry->typtype == TYPTYPE_DOMAIN)
+             return lookup_rowtype_tupdesc_noerror(typentry->domainBaseType,
+                                                   typentry->domainBaseTypmod,
+                                                   noError);
+         if (typentry->tupDesc == NULL && !noError)
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("type %s is not composite",
+                             format_type_be(type_id))));
+         tupDesc = typentry->tupDesc;
+     }
+     else
+         tupDesc = lookup_rowtype_tupdesc_internal(type_id, typmod, noError);
+     if (tupDesc != NULL)
+         PinTupleDesc(tupDesc);
+     return tupDesc;
+ }
+
+ /*
   * Hash function for the hash table of RecordCacheEntry.
   */
  static uint32
*************** TypeCacheRelCallback(Datum arg, Oid reli
*** 1929,1957 ****
      hash_seq_init(&status, TypeCacheHash);
      while ((typentry = (TypeCacheEntry *) hash_seq_search(&status)) != NULL)
      {
!         if (typentry->typtype != TYPTYPE_COMPOSITE)
!             continue;            /* skip non-composites */

!         /* Skip if no match, unless we're zapping all composite types */
!         if (relid != typentry->typrelid && relid != InvalidOid)
!             continue;

!         /* Delete tupdesc if we have it */
!         if (typentry->tupDesc != NULL)
          {
              /*
!              * Release our refcount, and free the tupdesc if none remain.
!              * (Can't use DecrTupleDescRefCount because this reference is not
!              * logged in current resource owner.)
               */
!             Assert(typentry->tupDesc->tdrefcount > 0);
!             if (--typentry->tupDesc->tdrefcount == 0)
!                 FreeTupleDesc(typentry->tupDesc);
!             typentry->tupDesc = NULL;
          }
-
-         /* Reset equality/comparison/hashing validity information */
-         typentry->flags = 0;
      }
  }

--- 2008,2047 ----
      hash_seq_init(&status, TypeCacheHash);
      while ((typentry = (TypeCacheEntry *) hash_seq_search(&status)) != NULL)
      {
!         if (typentry->typtype == TYPTYPE_COMPOSITE)
!         {
!             /* Skip if no match, unless we're zapping all composite types */
!             if (relid != typentry->typrelid && relid != InvalidOid)
!                 continue;

!             /* Delete tupdesc if we have it */
!             if (typentry->tupDesc != NULL)
!             {
!                 /*
!                  * Release our refcount, and free the tupdesc if none remain.
!                  * (Can't use DecrTupleDescRefCount because this reference is
!                  * not logged in current resource owner.)
!                  */
!                 Assert(typentry->tupDesc->tdrefcount > 0);
!                 if (--typentry->tupDesc->tdrefcount == 0)
!                     FreeTupleDesc(typentry->tupDesc);
!                 typentry->tupDesc = NULL;
!             }

!             /* Reset equality/comparison/hashing validity information */
!             typentry->flags = 0;
!         }
!         else if (typentry->typtype == TYPTYPE_DOMAIN)
          {
              /*
!              * If it's domain over composite, reset flags.  (We don't bother
!              * trying to determine whether the specific base type needs a
!              * reset.)  Note that if we haven't determined whether the base
!              * type is composite, we don't need to reset anything.
               */
!             if (typentry->flags & TCFLAGS_DOMAIN_BASE_IS_COMPOSITE)
!                 typentry->flags = 0;
          }
      }
  }

diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c
index 9c3f451..f4a3cce 100644
*** a/src/backend/utils/fmgr/funcapi.c
--- b/src/backend/utils/fmgr/funcapi.c
*************** static TypeFuncClass internal_get_result
*** 39,45 ****
  static bool resolve_polymorphic_tupdesc(TupleDesc tupdesc,
                              oidvector *declared_args,
                              Node *call_expr);
! static TypeFuncClass get_type_func_class(Oid typid);


  /*
--- 39,45 ----
  static bool resolve_polymorphic_tupdesc(TupleDesc tupdesc,
                              oidvector *declared_args,
                              Node *call_expr);
! static TypeFuncClass get_type_func_class(Oid typid, Oid *base_typeid);


  /*
*************** get_expr_result_type(Node *expr,
*** 246,259 ****
      {
          /* handle as a generic expression; no chance to resolve RECORD */
          Oid            typid = exprType(expr);

          if (resultTypeId)
              *resultTypeId = typid;
          if (resultTupleDesc)
              *resultTupleDesc = NULL;
!         result = get_type_func_class(typid);
!         if (result == TYPEFUNC_COMPOSITE && resultTupleDesc)
!             *resultTupleDesc = lookup_rowtype_tupdesc_copy(typid, -1);
      }

      return result;
--- 246,262 ----
      {
          /* handle as a generic expression; no chance to resolve RECORD */
          Oid            typid = exprType(expr);
+         Oid            base_typid;

          if (resultTypeId)
              *resultTypeId = typid;
          if (resultTupleDesc)
              *resultTupleDesc = NULL;
!         result = get_type_func_class(typid, &base_typid);
!         if ((result == TYPEFUNC_COMPOSITE ||
!              result == TYPEFUNC_COMPOSITE_DOMAIN) &&
!             resultTupleDesc)
!             *resultTupleDesc = lookup_rowtype_tupdesc_copy(base_typid, -1);
      }

      return result;
*************** internal_get_result_type(Oid funcid,
*** 296,301 ****
--- 299,305 ----
      HeapTuple    tp;
      Form_pg_proc procform;
      Oid            rettype;
+     Oid            base_rettype;
      TupleDesc    tupdesc;

      /* First fetch the function's pg_proc row to inspect its rettype */
*************** internal_get_result_type(Oid funcid,
*** 363,374 ****
          *resultTupleDesc = NULL;    /* default result */

      /* Classify the result type */
!     result = get_type_func_class(rettype);
      switch (result)
      {
          case TYPEFUNC_COMPOSITE:
              if (resultTupleDesc)
!                 *resultTupleDesc = lookup_rowtype_tupdesc_copy(rettype, -1);
              /* Named composite types can't have any polymorphic columns */
              break;
          case TYPEFUNC_SCALAR:
--- 367,379 ----
          *resultTupleDesc = NULL;    /* default result */

      /* Classify the result type */
!     result = get_type_func_class(rettype, &base_rettype);
      switch (result)
      {
          case TYPEFUNC_COMPOSITE:
+         case TYPEFUNC_COMPOSITE_DOMAIN:
              if (resultTupleDesc)
!                 *resultTupleDesc = lookup_rowtype_tupdesc_copy(base_rettype, -1);
              /* Named composite types can't have any polymorphic columns */
              break;
          case TYPEFUNC_SCALAR:
*************** internal_get_result_type(Oid funcid,
*** 394,399 ****
--- 399,444 ----
  }

  /*
+  * get_expr_result_tupdesc
+  *        Get a tupdesc describing the result of a composite-valued expression
+  *
+  * If expression is not composite or rowtype can't be determined, returns NULL
+  * if noError is true, else throws error.
+  *
+  * This is a simpler version of get_expr_result_type() for use when the caller
+  * is only interested in determinate rowtype results.
+  */
+ TupleDesc
+ get_expr_result_tupdesc(Node *expr, bool noError)
+ {
+     TupleDesc    tupleDesc;
+     TypeFuncClass functypclass;
+
+     functypclass = get_expr_result_type(expr, NULL, &tupleDesc);
+
+     if (functypclass == TYPEFUNC_COMPOSITE ||
+         functypclass == TYPEFUNC_COMPOSITE_DOMAIN)
+         return tupleDesc;
+
+     if (!noError)
+     {
+         Oid            exprTypeId = exprType(expr);
+
+         if (exprTypeId != RECORDOID)
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("type %s is not composite",
+                             format_type_be(exprTypeId))));
+         else
+             ereport(ERROR,
+                     (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                      errmsg("record type has not been registered")));
+     }
+
+     return NULL;
+ }
+
+ /*
   * Given the result tuple descriptor for a function with OUT parameters,
   * replace any polymorphic columns (ANYELEMENT etc) with correct data types
   * deduced from the input arguments. Returns TRUE if able to deduce all types,
*************** resolve_polymorphic_argtypes(int numargs
*** 741,763 ****
  /*
   * get_type_func_class
   *        Given the type OID, obtain its TYPEFUNC classification.
   *
   * This is intended to centralize a bunch of formerly ad-hoc code for
   * classifying types.  The categories used here are useful for deciding
   * how to handle functions returning the datatype.
   */
  static TypeFuncClass
! get_type_func_class(Oid typid)
  {
      switch (get_typtype(typid))
      {
          case TYPTYPE_COMPOSITE:
              return TYPEFUNC_COMPOSITE;
          case TYPTYPE_BASE:
-         case TYPTYPE_DOMAIN:
          case TYPTYPE_ENUM:
          case TYPTYPE_RANGE:
              return TYPEFUNC_SCALAR;
          case TYPTYPE_PSEUDO:
              if (typid == RECORDOID)
                  return TYPEFUNC_RECORD;
--- 786,816 ----
  /*
   * get_type_func_class
   *        Given the type OID, obtain its TYPEFUNC classification.
+  *        Also, if it's a domain, return the base type OID.
   *
   * This is intended to centralize a bunch of formerly ad-hoc code for
   * classifying types.  The categories used here are useful for deciding
   * how to handle functions returning the datatype.
   */
  static TypeFuncClass
! get_type_func_class(Oid typid, Oid *base_typeid)
  {
+     *base_typeid = typid;
+
      switch (get_typtype(typid))
      {
          case TYPTYPE_COMPOSITE:
              return TYPEFUNC_COMPOSITE;
          case TYPTYPE_BASE:
          case TYPTYPE_ENUM:
          case TYPTYPE_RANGE:
              return TYPEFUNC_SCALAR;
+         case TYPTYPE_DOMAIN:
+             *base_typeid = typid = getBaseType(typid);
+             if (get_typtype(typid) == TYPTYPE_COMPOSITE)
+                 return TYPEFUNC_COMPOSITE_DOMAIN;
+             else                /* domain base type can't be a pseudotype */
+                 return TYPEFUNC_SCALAR;
          case TYPTYPE_PSEUDO:
              if (typid == RECORDOID)
                  return TYPEFUNC_RECORD;
*************** RelationNameGetTupleDesc(const char *rel
*** 1320,1335 ****
  TupleDesc
  TypeGetTupleDesc(Oid typeoid, List *colaliases)
  {
!     TypeFuncClass functypclass = get_type_func_class(typeoid);
      TupleDesc    tupdesc = NULL;

      /*
!      * Build a suitable tupledesc representing the output rows
       */
      if (functypclass == TYPEFUNC_COMPOSITE)
      {
          /* Composite data type, e.g. a table's row type */
!         tupdesc = lookup_rowtype_tupdesc_copy(typeoid, -1);

          if (colaliases != NIL)
          {
--- 1373,1392 ----
  TupleDesc
  TypeGetTupleDesc(Oid typeoid, List *colaliases)
  {
!     Oid            base_typeoid;
!     TypeFuncClass functypclass = get_type_func_class(typeoid, &base_typeoid);
      TupleDesc    tupdesc = NULL;

      /*
!      * Build a suitable tupledesc representing the output rows.  We
!      * intentionally do not support TYPEFUNC_COMPOSITE_DOMAIN here, as it's
!      * unlikely that legacy callers of this obsolete function would be
!      * prepared to apply domain constraints.
       */
      if (functypclass == TYPEFUNC_COMPOSITE)
      {
          /* Composite data type, e.g. a table's row type */
!         tupdesc = lookup_rowtype_tupdesc_copy(base_typeoid, -1);

          if (colaliases != NIL)
          {
diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h
index fa04a63..b0d4c54 100644
*** a/src/include/access/htup_details.h
--- b/src/include/access/htup_details.h
*************** typedef struct DatumTupleFields
*** 134,139 ****
--- 134,144 ----
      Oid            datum_typeid;    /* composite type OID, or RECORDOID */

      /*
+      * datum_typeid cannot be a domain over composite, only plain composite,
+      * even if the datum is meant as a value of a domain-over-composite type.
+      * This is in line with the general principle that CoerceToDomain does not
+      * change the physical representation of the base type value.
+      *
       * Note: field ordering is chosen with thought that Oid might someday
       * widen to 64 bits.
       */
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index c15610e..2be5af1 100644
*** a/src/include/access/tupdesc.h
--- b/src/include/access/tupdesc.h
*************** typedef struct tupleConstr
*** 60,65 ****
--- 60,71 ----
   * row type, or a value >= 0 to allow the rowtype to be looked up in the
   * typcache.c type cache.
   *
+  * Note that tdtypeid is never the OID of a domain over composite, even if
+  * we are dealing with values that are known (at some higher level) to be of
+  * a domain-over-composite type.  This is because tdtypeid/tdtypmod need to
+  * match up with the type labeling of composite Datums, and those are never
+  * explicitly marked as being of a domain type, either.
+  *
   * Tuple descriptors that live in caches (relcache or typcache, at present)
   * are reference-counted: they can be deleted when their reference count goes
   * to zero.  Tuple descriptors created by the executor need no reference
diff --git a/src/include/funcapi.h b/src/include/funcapi.h
index 951af2a..7c52de9 100644
*** a/src/include/funcapi.h
--- b/src/include/funcapi.h
*************** typedef struct FuncCallContext
*** 143,148 ****
--- 143,152 ----
   *        get_call_result_type.  Note: the cases in which rowtypes cannot be
   *        determined are different from the cases for get_call_result_type.
   *        Do *not* use this if you can use one of the others.
+  *
+  * See also get_expr_result_tupdesc(), which is a convenient wrapper around
+  * get_expr_result_type() for use when the caller only cares about
+  * determinable-rowtype cases.
   *----------
   */

*************** typedef enum TypeFuncClass
*** 151,156 ****
--- 155,161 ----
  {
      TYPEFUNC_SCALAR,            /* scalar result type */
      TYPEFUNC_COMPOSITE,            /* determinable rowtype result */
+     TYPEFUNC_COMPOSITE_DOMAIN,    /* domain over determinable rowtype result */
      TYPEFUNC_RECORD,            /* indeterminate rowtype result */
      TYPEFUNC_OTHER                /* bogus type, eg pseudotype */
  } TypeFuncClass;
*************** extern TypeFuncClass get_func_result_typ
*** 165,170 ****
--- 170,177 ----
                       Oid *resultTypeId,
                       TupleDesc *resultTupleDesc);

+ extern TupleDesc get_expr_result_tupdesc(Node *expr, bool noError);
+
  extern bool resolve_polymorphic_argtypes(int numargs, Oid *argtypes,
                               char *argmodes,
                               Node *call_expr);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index ccb5123..c2929ac 100644
*** a/src/include/nodes/primnodes.h
--- b/src/include/nodes/primnodes.h
*************** typedef struct Var
*** 166,172 ****
      Index        varno;            /* index of this var's relation in the range
                                   * table, or INNER_VAR/OUTER_VAR/INDEX_VAR */
      AttrNumber    varattno;        /* attribute number of this var, or zero for
!                                  * all */
      Oid            vartype;        /* pg_type OID for the type of this var */
      int32        vartypmod;        /* pg_attribute typmod value */
      Oid            varcollid;        /* OID of collation, or InvalidOid if none */
--- 166,172 ----
      Index        varno;            /* index of this var's relation in the range
                                   * table, or INNER_VAR/OUTER_VAR/INDEX_VAR */
      AttrNumber    varattno;        /* attribute number of this var, or zero for
!                                  * all attrs ("whole-row Var") */
      Oid            vartype;        /* pg_type OID for the type of this var */
      int32        vartypmod;        /* pg_attribute typmod value */
      Oid            varcollid;        /* OID of collation, or InvalidOid if none */
*************** typedef struct FieldSelect
*** 755,760 ****
--- 755,763 ----
   * the assign case of ArrayRef, this is used to implement UPDATE of a
   * portion of a column.
   *
+  * resulttype is always a named composite type (not a domain).  To update
+  * a composite domain value, apply CoerceToDomain to the FieldStore.
+  *
   * A single FieldStore can actually represent updates of several different
   * fields.  The parser only generates FieldStores with single-element lists,
   * but the planner will collapse multiple updates of the same base column
*************** typedef struct ArrayCoerceExpr
*** 849,855 ****
   * needed for the destination type plus possibly others; the columns need not
   * be in the same positions, but are matched up by name.  This is primarily
   * used to convert a whole-row value of an inheritance child table into a
!  * valid whole-row value of its parent table's rowtype.
   * ----------------
   */

--- 852,859 ----
   * needed for the destination type plus possibly others; the columns need not
   * be in the same positions, but are matched up by name.  This is primarily
   * used to convert a whole-row value of an inheritance child table into a
!  * valid whole-row value of its parent table's rowtype.  Both resulttype
!  * and the exposed type of "arg" must be named composite types (not domains).
   * ----------------
   */

*************** typedef struct RowExpr
*** 987,992 ****
--- 991,999 ----
      Oid            row_typeid;        /* RECORDOID or a composite type's ID */

      /*
+      * row_typeid cannot be a domain over composite, only plain composite.  To
+      * create a composite domain value, apply CoerceToDomain to the RowExpr.
+      *
       * Note: we deliberately do NOT store a typmod.  Although a typmod will be
       * associated with specific RECORD types at runtime, it will differ for
       * different backends, and so cannot safely be stored in stored
diff --git a/src/include/parser/parse_type.h b/src/include/parser/parse_type.h
index 7b843d0..af1e314 100644
*** a/src/include/parser/parse_type.h
--- b/src/include/parser/parse_type.h
*************** extern Oid    typeTypeCollation(Type typ);
*** 46,55 ****
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! #define ISCOMPLEX(typeid) (typeidTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
--- 46,57 ----
  extern Datum stringTypeDatum(Type tp, char *string, int32 atttypmod);

  extern Oid    typeidTypeRelid(Oid type_id);
+ extern Oid    typeOrDomainTypeRelid(Oid type_id);

  extern TypeName *typeStringToTypeName(const char *str);
  extern void parseTypeString(const char *str, Oid *typeid_p, int32 *typmod_p, bool missing_ok);

! /* true if typeid is composite, or domain over composite, but not RECORD */
! #define ISCOMPLEX(typeid) (typeOrDomainTypeRelid(typeid) != InvalidOid)

  #endif                            /* PARSE_TYPE_H */
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 41b645a..ea799a8 100644
*** a/src/include/utils/typcache.h
--- b/src/include/utils/typcache.h
*************** typedef struct TypeCacheEntry
*** 92,97 ****
--- 92,104 ----
      FmgrInfo    rng_subdiff_finfo;    /* difference function, if any */

      /*
+      * Domain's base type and typmod if it's a domain type.  Zeroes if not
+      * domain, or if information hasn't been requested.
+      */
+     Oid            domainBaseType;
+     int32        domainBaseTypmod;
+
+     /*
       * Domain constraint data if it's a domain type.  NULL if not domain, or
       * if domain has no constraints, or if information hasn't been requested.
       */
*************** typedef struct TypeCacheEntry
*** 123,131 ****
  #define TYPECACHE_BTREE_OPFAMILY    0x0200
  #define TYPECACHE_HASH_OPFAMILY        0x0400
  #define TYPECACHE_RANGE_INFO        0x0800
! #define TYPECACHE_DOMAIN_INFO        0x1000
! #define TYPECACHE_HASH_EXTENDED_PROC        0x2000
! #define TYPECACHE_HASH_EXTENDED_PROC_FINFO    0x4000

  /*
   * Callers wishing to maintain a long-lived reference to a domain's constraint
--- 130,139 ----
  #define TYPECACHE_BTREE_OPFAMILY    0x0200
  #define TYPECACHE_HASH_OPFAMILY        0x0400
  #define TYPECACHE_RANGE_INFO        0x0800
! #define TYPECACHE_DOMAIN_BASE_INFO            0x1000
! #define TYPECACHE_DOMAIN_CONSTR_INFO        0x2000
! #define TYPECACHE_HASH_EXTENDED_PROC        0x4000
! #define TYPECACHE_HASH_EXTENDED_PROC_FINFO    0x8000

  /*
   * Callers wishing to maintain a long-lived reference to a domain's constraint
*************** extern TupleDesc lookup_rowtype_tupdesc_
*** 163,168 ****
--- 171,179 ----

  extern TupleDesc lookup_rowtype_tupdesc_copy(Oid type_id, int32 typmod);

+ extern TupleDesc lookup_rowtype_tupdesc_domain(Oid type_id, int32 typmod,
+                               bool noError);
+
  extern void assign_record_type_typmod(TupleDesc tupDesc);

  extern int    compare_values_of_enum(TypeCacheEntry *tcache, Oid arg1, Oid arg2);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 1e62c57..f7f3948 100644
*** a/src/test/regress/expected/domain.out
--- b/src/test/regress/expected/domain.out
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 198,203 ****
--- 198,291 ----
  (1 row)

  drop domain dia;
+ -- Test domains over composites
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ ERROR:  duplicate key value violates unique constraint "dcomptable_d1_key"
+ DETAIL:  Key (d1)=((1,2)) already exists.
+ insert into dcomptable (d1.r) values(11);
+ select * from dcomptable;
+   d1
+ -------
+  (1,2)
+  (3,4)
+  (11,)
+ (3 rows)
+
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+  r  | i | r  | i
+ ----+---+----+---
+   1 | 2 |  1 | 2
+   3 | 4 |  3 | 4
+  11 |   | 11 |
+ (3 rows)
+
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+   d1
+ -------
+  (11,)
+  (2,2)
+  (4,4)
+ (3 rows)
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+ ERROR:  column "d1" of table "dcomptable" contains values that violate the new constraint
+ select row(2,1)::dcomptype;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ ERROR:  value for domain dcomptype violates check constraint "c1"
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+     d1
+ ----------
+  (11,)
+  (99,)
+  (1,3)
+  (3,5)
+  (0,3)
+  (98,101)
+ (6 rows)
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+                                           QUERY PLAN
+ -----------------------------------------------------------------------------------------------
+  Update on public.dcomptable
+    ->  Seq Scan on public.dcomptable
+          Output: ROW(((d1).r - '1'::double precision), ((d1).i + '1'::double precision)), ctid
+          Filter: ((dcomptable.d1).i > '0'::double precision)
+ (4 rows)
+
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+                                   Table "public.dcomptable"
+  Column |   Type    | Collation | Nullable | Default | Storage  | Stats target | Description
+ --------+-----------+-----------+----------+---------+----------+--------------+-------------
+  d1     | dcomptype |           |          |         | extended |              |
+ Indexes:
+     "dcomptable_d1_key" UNIQUE CONSTRAINT, btree (d1)
+ Rules:
+     silly AS
+     ON DELETE TO dcomptable DO INSTEAD  UPDATE dcomptable SET d1.r = (dcomptable.d1).r - 1::double precision, d1.i =
(dcomptable.d1).i+ 1::double precision 
+   WHERE (dcomptable.d1).i > 0::double precision
+
+ drop table dcomptable;
+ drop type comptype cascade;
+ NOTICE:  drop cascades to type dcomptype
  -- Test domains over arrays of composite
  create type comptype as (r float8, i float8);
  create domain dcomptypea as comptype[];
*************** insert into ddtest2 values('{(-1)}');
*** 762,767 ****
--- 850,863 ----
  alter domain posint add constraint c1 check(value >= 0);
  ERROR:  cannot alter type "posint" because column "ddtest2.f1" uses it
  drop table ddtest2;
+ -- Likewise for domains within domains over composite
+ create domain ddtest1d as ddtest1;
+ create table ddtest2(f1 ddtest1d);
+ insert into ddtest2 values('(-1)');
+ alter domain posint add constraint c1 check(value >= 0);
+ ERROR:  cannot alter type "posint" because column "ddtest2.f1" uses it
+ drop table ddtest2;
+ drop domain ddtest1d;
  -- Likewise for domains within domains over array of composite
  create domain ddtest1d as ddtest1[];
  create table ddtest2(f1 ddtest1d);
diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out
index d7abae9..f57d1ab 100644
*** a/src/test/regress/expected/json.out
--- b/src/test/regress/expected/json.out
*************** create type jpop as (a text, b int, c ti
*** 1316,1321 ****
--- 1316,1323 ----
  CREATE DOMAIN js_int_not_null  AS int     NOT NULL;
  CREATE DOMAIN js_int_array_1d  AS int[]   CHECK(array_length(VALUE, 1) = 3);
  CREATE DOMAIN js_int_array_2d  AS int[][] CHECK(array_length(VALUE, 2) = 3);
+ create type j_unordered_pair as (x int, y int);
+ create domain j_ordered_pair as j_unordered_pair check((value).x <= (value).y);
  CREATE TYPE jsrec AS (
      i    int,
      ia    _int4,
*************** SELECT rec FROM json_populate_record(
*** 1740,1745 ****
--- 1742,1771 ----
   (abc,3,"Thu Jan 02 00:00:00 2003")
  (1 row)

+ -- anonymous record type
+ SELECT json_populate_record(null::record, '{"x": 0, "y": 1}');
+ ERROR:  record type has not been registered
+ SELECT json_populate_record(row(1,2), '{"f1": 0, "f2": 1}');
+  json_populate_record
+ ----------------------
+  (0,1)
+ (1 row)
+
+ -- composite domain
+ SELECT json_populate_record(null::j_ordered_pair, '{"x": 0, "y": 1}');
+  json_populate_record
+ ----------------------
+  (0,1)
+ (1 row)
+
+ SELECT json_populate_record(row(1,2)::j_ordered_pair, '{"x": 0}');
+  json_populate_record
+ ----------------------
+  (0,2)
+ (1 row)
+
+ SELECT json_populate_record(row(1,2)::j_ordered_pair, '{"x": 1, "y": 0}');
+ ERROR:  value for domain j_ordered_pair violates check constraint "j_ordered_pair_check"
  -- populate_recordset
  select * from json_populate_recordset(null::jpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20 10:42:53"}]') q;
     a    | b |            c
*************** select * from json_populate_recordset(ro
*** 1806,1811 ****
--- 1832,1862 ----
   {"z":true}    |  3 | Fri Jan 20 10:42:53 2012
  (2 rows)

+ -- anonymous record type
+ SELECT json_populate_recordset(null::record, '[{"x": 0, "y": 1}]');
+ ERROR:  record type has not been registered
+ SELECT json_populate_recordset(row(1,2), '[{"f1": 0, "f2": 1}]');
+  json_populate_recordset
+ -------------------------
+  (0,1)
+ (1 row)
+
+ -- composite domain
+ SELECT json_populate_recordset(null::j_ordered_pair, '[{"x": 0, "y": 1}]');
+  json_populate_recordset
+ -------------------------
+  (0,1)
+ (1 row)
+
+ SELECT json_populate_recordset(row(1,2)::j_ordered_pair, '[{"x": 0}, {"y": 3}]');
+  json_populate_recordset
+ -------------------------
+  (0,2)
+  (1,3)
+ (2 rows)
+
+ SELECT json_populate_recordset(row(1,2)::j_ordered_pair, '[{"x": 1, "y": 0}]');
+ ERROR:  value for domain j_ordered_pair violates check constraint "j_ordered_pair_check"
  -- test type info caching in json_populate_record()
  CREATE TEMP TABLE jspoptest (js json);
  INSERT INTO jspoptest
*************** DROP TYPE jsrec_i_not_null;
*** 1828,1833 ****
--- 1879,1886 ----
  DROP DOMAIN js_int_not_null;
  DROP DOMAIN js_int_array_1d;
  DROP DOMAIN js_int_array_2d;
+ DROP DOMAIN j_ordered_pair;
+ DROP TYPE j_unordered_pair;
  --json_typeof() function
  select value, json_typeof(value)
    from (values (json '123.4'),
diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out
index dcea6a4..7f40ced 100644
*** a/src/test/regress/expected/jsonb.out
--- b/src/test/regress/expected/jsonb.out
*************** CREATE TYPE jbpop AS (a text, b int, c t
*** 1900,1905 ****
--- 1900,1907 ----
  CREATE DOMAIN jsb_int_not_null  AS int     NOT NULL;
  CREATE DOMAIN jsb_int_array_1d  AS int[]   CHECK(array_length(VALUE, 1) = 3);
  CREATE DOMAIN jsb_int_array_2d  AS int[][] CHECK(array_length(VALUE, 2) = 3);
+ create type jb_unordered_pair as (x int, y int);
+ create domain jb_ordered_pair as jb_unordered_pair check((value).x <= (value).y);
  CREATE TYPE jsbrec AS (
      i    int,
      ia    _int4,
*************** SELECT rec FROM jsonb_populate_record(
*** 2324,2329 ****
--- 2326,2355 ----
   (abc,3,"Thu Jan 02 00:00:00 2003")
  (1 row)

+ -- anonymous record type
+ SELECT jsonb_populate_record(null::record, '{"x": 0, "y": 1}');
+ ERROR:  record type has not been registered
+ SELECT jsonb_populate_record(row(1,2), '{"f1": 0, "f2": 1}');
+  jsonb_populate_record
+ -----------------------
+  (0,1)
+ (1 row)
+
+ -- composite domain
+ SELECT jsonb_populate_record(null::jb_ordered_pair, '{"x": 0, "y": 1}');
+  jsonb_populate_record
+ -----------------------
+  (0,1)
+ (1 row)
+
+ SELECT jsonb_populate_record(row(1,2)::jb_ordered_pair, '{"x": 0}');
+  jsonb_populate_record
+ -----------------------
+  (0,2)
+ (1 row)
+
+ SELECT jsonb_populate_record(row(1,2)::jb_ordered_pair, '{"x": 1, "y": 0}');
+ ERROR:  value for domain jb_ordered_pair violates check constraint "jb_ordered_pair_check"
  -- populate_recordset
  SELECT * FROM jsonb_populate_recordset(NULL::jbpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20 10:42:53"}]') q;
     a    | b |            c
*************** SELECT * FROM jsonb_populate_recordset(r
*** 2383,2388 ****
--- 2409,2439 ----
   {"z": true}     |  3 | Fri Jan 20 10:42:53 2012
  (2 rows)

+ -- anonymous record type
+ SELECT jsonb_populate_recordset(null::record, '[{"x": 0, "y": 1}]');
+ ERROR:  record type has not been registered
+ SELECT jsonb_populate_recordset(row(1,2), '[{"f1": 0, "f2": 1}]');
+  jsonb_populate_recordset
+ --------------------------
+  (0,1)
+ (1 row)
+
+ -- composite domain
+ SELECT jsonb_populate_recordset(null::jb_ordered_pair, '[{"x": 0, "y": 1}]');
+  jsonb_populate_recordset
+ --------------------------
+  (0,1)
+ (1 row)
+
+ SELECT jsonb_populate_recordset(row(1,2)::jb_ordered_pair, '[{"x": 0}, {"y": 3}]');
+  jsonb_populate_recordset
+ --------------------------
+  (0,2)
+  (1,3)
+ (2 rows)
+
+ SELECT jsonb_populate_recordset(row(1,2)::jb_ordered_pair, '[{"x": 1, "y": 0}]');
+ ERROR:  value for domain jb_ordered_pair violates check constraint "jb_ordered_pair_check"
  -- jsonb_to_record and jsonb_to_recordset
  select * from jsonb_to_record('{"a":1,"b":"foo","c":"bar"}')
      as x(a int, b text, d text);
*************** DROP TYPE jsbrec_i_not_null;
*** 2482,2487 ****
--- 2533,2540 ----
  DROP DOMAIN jsb_int_not_null;
  DROP DOMAIN jsb_int_array_1d;
  DROP DOMAIN jsb_int_array_2d;
+ DROP DOMAIN jb_ordered_pair;
+ DROP TYPE jb_unordered_pair;
  -- indexing
  SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}';
   count
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 8fb3e20..5201f00 100644
*** a/src/test/regress/sql/domain.sql
--- b/src/test/regress/sql/domain.sql
*************** select pg_typeof('{1,2,3}'::dia || 42);
*** 120,125 ****
--- 120,164 ----
  drop domain dia;


+ -- Test domains over composites
+
+ create type comptype as (r float8, i float8);
+ create domain dcomptype as comptype;
+ create table dcomptable (d1 dcomptype unique);
+
+ insert into dcomptable values (row(1,2)::dcomptype);
+ insert into dcomptable values (row(3,4)::comptype);
+ insert into dcomptable values (row(1,2)::dcomptype);  -- fail on uniqueness
+ insert into dcomptable (d1.r) values(11);
+
+ select * from dcomptable;
+ select (d1).r, (d1).i, (d1).* from dcomptable;
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ alter domain dcomptype add constraint c1 check ((value).r <= (value).i);
+ alter domain dcomptype add constraint c2 check ((value).r > (value).i);  -- fail
+
+ select row(2,1)::dcomptype;  -- fail
+ insert into dcomptable values (row(1,2)::comptype);
+ insert into dcomptable values (row(2,1)::comptype);  -- fail
+ insert into dcomptable (d1.r) values(99);
+ insert into dcomptable (d1.r, d1.i) values(99, 100);
+ insert into dcomptable (d1.r, d1.i) values(100, 99);  -- fail
+ update dcomptable set d1.r = (d1).r + 1 where (d1).i > 0;  -- fail
+ update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ select * from dcomptable;
+
+ explain (verbose, costs off)
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ create rule silly as on delete to dcomptable do instead
+   update dcomptable set d1.r = (d1).r - 1, d1.i = (d1).i + 1 where (d1).i > 0;
+ \d+ dcomptable
+
+ drop table dcomptable;
+ drop type comptype cascade;
+
+
  -- Test domains over arrays of composite

  create type comptype as (r float8, i float8);
*************** insert into ddtest2 values('{(-1)}');
*** 500,505 ****
--- 539,552 ----
  alter domain posint add constraint c1 check(value >= 0);
  drop table ddtest2;

+ -- Likewise for domains within domains over composite
+ create domain ddtest1d as ddtest1;
+ create table ddtest2(f1 ddtest1d);
+ insert into ddtest2 values('(-1)');
+ alter domain posint add constraint c1 check(value >= 0);
+ drop table ddtest2;
+ drop domain ddtest1d;
+
  -- Likewise for domains within domains over array of composite
  create domain ddtest1d as ddtest1[];
  create table ddtest2(f1 ddtest1d);
diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql
index 506e3a8..0859353 100644
*** a/src/test/regress/sql/json.sql
--- b/src/test/regress/sql/json.sql
*************** CREATE DOMAIN js_int_not_null  AS int
*** 388,393 ****
--- 388,396 ----
  CREATE DOMAIN js_int_array_1d  AS int[]   CHECK(array_length(VALUE, 1) = 3);
  CREATE DOMAIN js_int_array_2d  AS int[][] CHECK(array_length(VALUE, 2) = 3);

+ create type j_unordered_pair as (x int, y int);
+ create domain j_ordered_pair as j_unordered_pair check((value).x <= (value).y);
+
  CREATE TYPE jsrec AS (
      i    int,
      ia    _int4,
*************** SELECT rec FROM json_populate_record(
*** 516,521 ****
--- 519,533 ----
      '{"rec": {"a": "abc", "c": "01.02.2003", "x": 43.2}}'
  ) q;

+ -- anonymous record type
+ SELECT json_populate_record(null::record, '{"x": 0, "y": 1}');
+ SELECT json_populate_record(row(1,2), '{"f1": 0, "f2": 1}');
+
+ -- composite domain
+ SELECT json_populate_record(null::j_ordered_pair, '{"x": 0, "y": 1}');
+ SELECT json_populate_record(row(1,2)::j_ordered_pair, '{"x": 0}');
+ SELECT json_populate_record(row(1,2)::j_ordered_pair, '{"x": 1, "y": 0}');
+
  -- populate_recordset

  select * from json_populate_recordset(null::jpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20 10:42:53"}]') q;
*************** select * from json_populate_recordset(nu
*** 532,537 ****
--- 544,558 ----
  select * from json_populate_recordset(row('def',99,null)::jpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20
10:42:53"}]')q; 
  select * from
json_populate_recordset(row('def',99,null)::jpop,'[{"a":[100,200,300],"x":43.2},{"a":{"z":true},"b":3,"c":"2012-01-20
10:42:53"}]')q; 

+ -- anonymous record type
+ SELECT json_populate_recordset(null::record, '[{"x": 0, "y": 1}]');
+ SELECT json_populate_recordset(row(1,2), '[{"f1": 0, "f2": 1}]');
+
+ -- composite domain
+ SELECT json_populate_recordset(null::j_ordered_pair, '[{"x": 0, "y": 1}]');
+ SELECT json_populate_recordset(row(1,2)::j_ordered_pair, '[{"x": 0}, {"y": 3}]');
+ SELECT json_populate_recordset(row(1,2)::j_ordered_pair, '[{"x": 1, "y": 0}]');
+
  -- test type info caching in json_populate_record()
  CREATE TEMP TABLE jspoptest (js json);

*************** DROP TYPE jsrec_i_not_null;
*** 550,555 ****
--- 571,578 ----
  DROP DOMAIN js_int_not_null;
  DROP DOMAIN js_int_array_1d;
  DROP DOMAIN js_int_array_2d;
+ DROP DOMAIN j_ordered_pair;
+ DROP TYPE j_unordered_pair;

  --json_typeof() function
  select value, json_typeof(value)
diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql
index 57fff3b..9cad707 100644
*** a/src/test/regress/sql/jsonb.sql
--- b/src/test/regress/sql/jsonb.sql
*************** CREATE DOMAIN jsb_int_not_null  AS int
*** 488,493 ****
--- 488,496 ----
  CREATE DOMAIN jsb_int_array_1d  AS int[]   CHECK(array_length(VALUE, 1) = 3);
  CREATE DOMAIN jsb_int_array_2d  AS int[][] CHECK(array_length(VALUE, 2) = 3);

+ create type jb_unordered_pair as (x int, y int);
+ create domain jb_ordered_pair as jb_unordered_pair check((value).x <= (value).y);
+
  CREATE TYPE jsbrec AS (
      i    int,
      ia    _int4,
*************** SELECT rec FROM jsonb_populate_record(
*** 616,621 ****
--- 619,633 ----
      '{"rec": {"a": "abc", "c": "01.02.2003", "x": 43.2}}'
  ) q;

+ -- anonymous record type
+ SELECT jsonb_populate_record(null::record, '{"x": 0, "y": 1}');
+ SELECT jsonb_populate_record(row(1,2), '{"f1": 0, "f2": 1}');
+
+ -- composite domain
+ SELECT jsonb_populate_record(null::jb_ordered_pair, '{"x": 0, "y": 1}');
+ SELECT jsonb_populate_record(row(1,2)::jb_ordered_pair, '{"x": 0}');
+ SELECT jsonb_populate_record(row(1,2)::jb_ordered_pair, '{"x": 1, "y": 0}');
+
  -- populate_recordset
  SELECT * FROM jsonb_populate_recordset(NULL::jbpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20 10:42:53"}]') q;
  SELECT * FROM jsonb_populate_recordset(row('def',99,NULL)::jbpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20
10:42:53"}]')q; 
*************** SELECT * FROM jsonb_populate_recordset(N
*** 628,633 ****
--- 640,654 ----
  SELECT * FROM jsonb_populate_recordset(row('def',99,NULL)::jbpop,'[{"a":"blurfl","x":43.2},{"b":3,"c":"2012-01-20
10:42:53"}]')q; 
  SELECT * FROM
jsonb_populate_recordset(row('def',99,NULL)::jbpop,'[{"a":[100,200,300],"x":43.2},{"a":{"z":true},"b":3,"c":"2012-01-20
10:42:53"}]')q; 

+ -- anonymous record type
+ SELECT jsonb_populate_recordset(null::record, '[{"x": 0, "y": 1}]');
+ SELECT jsonb_populate_recordset(row(1,2), '[{"f1": 0, "f2": 1}]');
+
+ -- composite domain
+ SELECT jsonb_populate_recordset(null::jb_ordered_pair, '[{"x": 0, "y": 1}]');
+ SELECT jsonb_populate_recordset(row(1,2)::jb_ordered_pair, '[{"x": 0}, {"y": 3}]');
+ SELECT jsonb_populate_recordset(row(1,2)::jb_ordered_pair, '[{"x": 1, "y": 0}]');
+
  -- jsonb_to_record and jsonb_to_recordset

  select * from jsonb_to_record('{"a":1,"b":"foo","c":"bar"}')
*************** DROP TYPE jsbrec_i_not_null;
*** 673,678 ****
--- 694,701 ----
  DROP DOMAIN jsb_int_not_null;
  DROP DOMAIN jsb_int_array_1d;
  DROP DOMAIN jsb_int_array_2d;
+ DROP DOMAIN jb_ordered_pair;
+ DROP TYPE jb_unordered_pair;

  -- indexing
  SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}';

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers