CONSTANT/NOT NULL/initializer properties for plpgsql record variables - Mailing list pgsql-hackers

From Tom Lane
Subject CONSTANT/NOT NULL/initializer properties for plpgsql record variables
Date
Msg-id 18362.1514605650@sss.pgh.pa.us
Whole thread Raw
Responses Re: CONSTANT/NOT NULL/initializer properties for plpgsql record variables  (Tom Lane <tgl@sss.pgh.pa.us>)
List pgsql-hackers
I said a couple of times in recent threads that it wouldn't be too hard
to implement $SUBJECT given the other patches I've been working on.
Attached is proof of the pudding.  This needs to be applied on top of
the patches in
https://postgr.es/m/23537.1514589378@sss.pgh.pa.us
and
https://postgr.es/m/11986.1514407114@sss.pgh.pa.us

It's pretty straightforward really.  Worth noting is that this also
fixes the null-domain-value issues I mentioned as being a loose end
in the first of the above-referenced messages.  Also, I created a
new plpgsql test file for these features, and moved the one relevant
existing test case into that file.

I was a bit disappointed to find that no documentation changes seem
needed, because the SGML docs fail to acknowledge that these cases
didn't work ...

            regards, tom lane

diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile
index f9a3616..ad93e3e 100644
*** a/src/pl/plpgsql/src/Makefile
--- b/src/pl/plpgsql/src/Makefile
*************** DATA = plpgsql.control plpgsql--1.0.sql
*** 26,32 ****

  REGRESS_OPTS = --dbname=$(PL_TESTDB)

! REGRESS = plpgsql_call plpgsql_record

  all: all-lib

--- 26,32 ----

  REGRESS_OPTS = --dbname=$(PL_TESTDB)

! REGRESS = plpgsql_call plpgsql_record plpgsql_varprops

  all: all-lib

diff --git a/src/pl/plpgsql/src/expected/plpgsql_varprops.out b/src/pl/plpgsql/src/expected/plpgsql_varprops.out
index ...109056c .
*** a/src/pl/plpgsql/src/expected/plpgsql_varprops.out
--- b/src/pl/plpgsql/src/expected/plpgsql_varprops.out
***************
*** 0 ****
--- 1,300 ----
+ --
+ -- Tests for PL/pgSQL variable properties: CONSTANT, NOT NULL, initializers
+ --
+ create type var_record as (f1 int4, f2 int4);
+ create domain int_nn as int not null;
+ create domain var_record_nn as var_record not null;
+ create domain var_record_colnn as var_record check((value).f2 is not null);
+ -- CONSTANT
+ do $$
+ declare x constant int := 42;
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ NOTICE:  x = 42
+ do $$
+ declare x constant int;
+ begin
+   x := 42;  -- fail
+ end$$;
+ ERROR:  variable "x" is declared CONSTANT
+ LINE 4:   x := 42;  -- fail
+           ^
+ do $$
+ declare x constant int; y int;
+ begin
+   for x, y in select 1, 2 loop  -- fail
+   end loop;
+ end$$;
+ ERROR:  variable "x" is declared CONSTANT
+ LINE 4:   for x, y in select 1, 2 loop  -- fail
+               ^
+ do $$
+ declare x constant int[];
+ begin
+   x[1] := 42;  -- fail
+ end$$;
+ ERROR:  variable "x" is declared CONSTANT
+ LINE 4:   x[1] := 42;  -- fail
+           ^
+ do $$
+ declare x constant int[]; y int;
+ begin
+   for x[1], y in select 1, 2 loop  -- fail (currently, unsupported syntax)
+   end loop;
+ end$$;
+ ERROR:  syntax error at or near "["
+ LINE 4:   for x[1], y in select 1, 2 loop  -- fail (currently, unsup...
+                ^
+ do $$
+ declare x constant var_record;
+ begin
+   x.f1 := 42;  -- fail
+ end$$;
+ ERROR:  variable "x" is declared CONSTANT
+ LINE 4:   x.f1 := 42;  -- fail
+           ^
+ do $$
+ declare x constant var_record; y int;
+ begin
+   for x.f1, y in select 1, 2 loop  -- fail
+   end loop;
+ end$$;
+ ERROR:  variable "x" is declared CONSTANT
+ LINE 4:   for x.f1, y in select 1, 2 loop  -- fail
+               ^
+ -- initializer expressions
+ do $$
+ declare x int := sin(0);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ NOTICE:  x = 0
+ do $$
+ declare x int := 1/0;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT 1/0"
+ PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x bigint[] := array[1,3,5];
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ NOTICE:  x = {1,3,5}
+ do $$
+ declare x record := row(1,2,3);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ NOTICE:  x = (1,2,3)
+ do $$
+ declare x var_record := row(1,2);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ NOTICE:  x = (1,2)
+ -- NOT NULL
+ do $$
+ declare x int not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  variable "x" must have a default value, since it's declared NOT NULL
+ LINE 2: declare x int not null;  -- fail
+                       ^
+ do $$
+ declare x int not null := 42;
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = 42
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 5 at assignment
+ do $$
+ declare x int not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x record not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  variable "x" must have a default value, since it's declared NOT NULL
+ LINE 2: declare x record not null;  -- fail
+                          ^
+ do $$
+ declare x record not null := row(42);
+ begin
+   raise notice 'x = %', x;
+   x := row(null);  -- ok
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = (42)
+ NOTICE:  x = ()
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 7 at assignment
+ do $$
+ declare x record not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  variable "x" must have a default value, since it's declared NOT NULL
+ LINE 2: declare x var_record not null;  -- fail
+                              ^
+ do $$
+ declare x var_record not null := row(41,42);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- ok
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = (41,42)
+ NOTICE:  x = (,)
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 7 at assignment
+ do $$
+ declare x var_record not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  null value cannot be assigned to variable "x" declared NOT NULL
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ -- Check that variables are reinitialized on block re-entry.
+ \set VERBOSITY terse   \\ -- needed for output stability
+ do $$
+ begin
+   for i in 1..3 loop
+     declare
+       x int;
+       y int := i;
+       r record;
+       c var_record;
+     begin
+       if i = 1 then
+         x := 42;
+         r := row(i, i+1);
+         c := row(i, i+1);
+       end if;
+       raise notice 'x = %', x;
+       raise notice 'y = %', y;
+       raise notice 'r = %', r;
+       raise notice 'c = %', c;
+     end;
+   end loop;
+ end$$;
+ NOTICE:  x = 42
+ NOTICE:  y = 1
+ NOTICE:  r = (1,2)
+ NOTICE:  c = (1,2)
+ NOTICE:  x = <NULL>
+ NOTICE:  y = 2
+ NOTICE:  r = <NULL>
+ NOTICE:  c = <NULL>
+ NOTICE:  x = <NULL>
+ NOTICE:  y = 3
+ NOTICE:  r = <NULL>
+ NOTICE:  c = <NULL>
+ \set VERBOSITY default
+ -- Check enforcement of domain constraints during initialization
+ do $$
+ declare x int_nn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  domain int_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x int_nn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  domain int_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x int_nn := 42;
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = 42
+ ERROR:  domain int_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 5 at assignment
+ do $$
+ declare x var_record_nn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  domain var_record_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record_nn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  domain var_record_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record_nn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- ok
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = (1,2)
+ ERROR:  domain var_record_nn does not allow null values
+ CONTEXT:  PL/pgSQL function inline_code_block line 6 at assignment
+ do $$
+ declare x var_record_colnn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  value for domain var_record_colnn violates check constraint "var_record_colnn_check"
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record_colnn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  value for domain var_record_colnn violates check constraint "var_record_colnn_check"
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record_colnn := row(1,null);  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+ ERROR:  value for domain var_record_colnn violates check constraint "var_record_colnn_check"
+ CONTEXT:  PL/pgSQL function inline_code_block line 3 during statement block local variable initialization
+ do $$
+ declare x var_record_colnn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+ NOTICE:  x = (1,2)
+ ERROR:  value for domain var_record_colnn violates check constraint "var_record_colnn_check"
+ CONTEXT:  PL/pgSQL function inline_code_block line 5 at assignment
+ do $$
+ declare x var_record_colnn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- fail
+ end$$;
+ NOTICE:  x = (1,2)
+ ERROR:  value for domain var_record_colnn violates check constraint "var_record_colnn_check"
+ CONTEXT:  PL/pgSQL function inline_code_block line 5 at assignment
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 5c2e463..576924f 100644
*** a/src/pl/plpgsql/src/pl_comp.c
--- b/src/pl/plpgsql/src/pl_comp.c
*************** do_compile(FunctionCallInfo fcinfo,
*** 594,604 ****
                           errhint("The arguments of the trigger can be accessed through TG_NARGS and TG_ARGV
instead.")));

              /* Add the record for referencing NEW ROW */
!             rec = plpgsql_build_record("new", 0, RECORDOID, true);
              function->new_varno = rec->dno;

              /* Add the record for referencing OLD ROW */
!             rec = plpgsql_build_record("old", 0, RECORDOID, true);
              function->old_varno = rec->dno;

              /* Add the variable tg_name */
--- 594,604 ----
                           errhint("The arguments of the trigger can be accessed through TG_NARGS and TG_ARGV
instead.")));

              /* Add the record for referencing NEW ROW */
!             rec = plpgsql_build_record("new", 0, NULL, RECORDOID, true);
              function->new_varno = rec->dno;

              /* Add the record for referencing OLD ROW */
!             rec = plpgsql_build_record("old", 0, NULL, RECORDOID, true);
              function->old_varno = rec->dno;

              /* Add the variable tg_name */
*************** plpgsql_build_variable(const char *refna
*** 1811,1817 ****
                  var->refname = pstrdup(refname);
                  var->lineno = lineno;
                  var->datatype = dtype;
!                 /* other fields might be filled by caller */

                  /* preset to NULL */
                  var->value = 0;
--- 1811,1817 ----
                  var->refname = pstrdup(refname);
                  var->lineno = lineno;
                  var->datatype = dtype;
!                 /* other fields are left as 0, might be changed by caller */

                  /* preset to NULL */
                  var->value = 0;
*************** plpgsql_build_variable(const char *refna
*** 1831,1837 ****
                  /* Composite type -- build a record variable */
                  PLpgSQL_rec *rec;

!                 rec = plpgsql_build_record(refname, lineno, dtype->typoid,
                                             add2namespace);
                  result = (PLpgSQL_variable *) rec;
                  break;
--- 1831,1838 ----
                  /* Composite type -- build a record variable */
                  PLpgSQL_rec *rec;

!                 rec = plpgsql_build_record(refname, lineno,
!                                            dtype, dtype->typoid,
                                             add2namespace);
                  result = (PLpgSQL_variable *) rec;
                  break;
*************** plpgsql_build_variable(const char *refna
*** 1856,1862 ****
   * Build empty named record variable, and optionally add it to namespace
   */
  PLpgSQL_rec *
! plpgsql_build_record(const char *refname, int lineno, Oid rectypeid,
                       bool add2namespace)
  {
      PLpgSQL_rec *rec;
--- 1857,1864 ----
   * Build empty named record variable, and optionally add it to namespace
   */
  PLpgSQL_rec *
! plpgsql_build_record(const char *refname, int lineno,
!                      PLpgSQL_type *dtype, Oid rectypeid,
                       bool add2namespace)
  {
      PLpgSQL_rec *rec;
*************** plpgsql_build_record(const char *refname
*** 1865,1870 ****
--- 1867,1874 ----
      rec->dtype = PLPGSQL_DTYPE_REC;
      rec->refname = pstrdup(refname);
      rec->lineno = lineno;
+     /* other fields are left as 0, might be changed by caller */
+     rec->datatype = dtype;
      rec->rectypeid = rectypeid;
      rec->firstfield = -1;
      rec->erh = NULL;
*************** build_row_from_vars(PLpgSQL_variable **v
*** 1899,1904 ****
--- 1903,1911 ----
          int32        typmod;
          Oid            typcoll;

+         /* Member vars of a row should never be const */
+         Assert(!var->isconst);
+
          switch (var->dtype)
          {
              case PLPGSQL_DTYPE_VAR:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index c9fbd83..e97be40 100644
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** plpgsql_exec_function(PLpgSQL_function *
*** 461,467 ****
                      }
                      else
                      {
!                         /* If arg is null, treat it as an empty row */
                          exec_move_row(&estate, (PLpgSQL_variable *) rec,
                                        NULL, NULL);
                      }
--- 461,467 ----
                      }
                      else
                      {
!                         /* If arg is null, set variable to null */
                          exec_move_row(&estate, (PLpgSQL_variable *) rec,
                                        NULL, NULL);
                      }
*************** exec_stmt_block(PLpgSQL_execstate *estat
*** 1461,1471 ****
                      {
                          /*
                           * If needed, give the datatype a chance to reject
!                          * NULLs, by assigning a NULL to the variable. We
                           * claim the value is of type UNKNOWN, not the var's
!                          * datatype, else coercion will be skipped. (Do this
!                          * before the notnull check to be consistent with
!                          * exec_assign_value.)
                           */
                          if (var->datatype->typtype == TYPTYPE_DOMAIN)
                              exec_assign_value(estate,
--- 1461,1469 ----
                      {
                          /*
                           * If needed, give the datatype a chance to reject
!                          * NULLs, by assigning a NULL to the variable.  We
                           * claim the value is of type UNKNOWN, not the var's
!                          * datatype, else coercion will be skipped.
                           */
                          if (var->datatype->typtype == TYPTYPE_DOMAIN)
                              exec_assign_value(estate,
*************** exec_stmt_block(PLpgSQL_execstate *estat
*** 1475,1485 ****
                                                UNKNOWNOID,
                                                -1);

!                         if (var->notnull)
!                             ereport(ERROR,
!                                     (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
!                                      errmsg("variable \"%s\" declared NOT NULL cannot default to NULL",
!                                             var->refname)));
                      }
                      else
                      {
--- 1473,1480 ----
                                                UNKNOWNOID,
                                                -1);

!                         /* parser should have rejected NOT NULL */
!                         Assert(!var->notnull);
                      }
                      else
                      {
*************** exec_stmt_block(PLpgSQL_execstate *estat
*** 1493,1501 ****
                  {
                      PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;

!                     if (rec->erh)
!                         DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
!                     rec->erh = NULL;
                  }
                  break;

--- 1488,1515 ----
                  {
                      PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;

!                     /*
!                      * Deletion of any existing object will be handled during
!                      * the assignments below, and in some cases it's more
!                      * efficient for us not to get rid of it beforehand.
!                      */
!                     if (rec->default_val == NULL)
!                     {
!                         /*
!                          * If needed, give the datatype a chance to reject
!                          * NULLs, by assigning a NULL to the variable.
!                          */
!                         exec_move_row(estate, (PLpgSQL_variable *) rec,
!                                       NULL, NULL);
!
!                         /* parser should have rejected NOT NULL */
!                         Assert(!rec->notnull);
!                     }
!                     else
!                     {
!                         exec_assign_expr(estate, (PLpgSQL_datum *) rec,
!                                          rec->default_val);
!                     }
                  }
                  break;

*************** exec_assign_value(PLpgSQL_execstate *est
*** 4739,4745 ****

                  if (isNull)
                  {
!                     /* If source is null, just assign nulls to the record */
                      exec_move_row(estate, (PLpgSQL_variable *) rec,
                                    NULL, NULL);
                  }
--- 4753,4765 ----

                  if (isNull)
                  {
!                     if (rec->notnull)
!                         ereport(ERROR,
!                                 (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
!                                  errmsg("null value cannot be assigned to variable \"%s\" declared NOT NULL",
!                                         rec->refname)));
!
!                     /* Set variable to a simple NULL */
                      exec_move_row(estate, (PLpgSQL_variable *) rec,
                                    NULL, NULL);
                  }
*************** exec_move_row(PLpgSQL_execstate *estate,
*** 6442,6450 ****
           */
          if (tupdesc == NULL)
          {
!             if (rec->erh)
!                 DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
!             rec->erh = NULL;
              return;
          }

--- 6462,6488 ----
           */
          if (tupdesc == NULL)
          {
!             if (rec->datatype &&
!                 rec->datatype->typtype == TYPTYPE_DOMAIN)
!             {
!                 /*
!                  * If it's a composite domain, NULL might not be a legal
!                  * value, so we instead need to make an empty expanded record
!                  * and ensure that domain type checking gets done.  If there
!                  * is already an expanded record, piggyback on its lookups.
!                  */
!                 newerh = make_expanded_record_for_rec(estate, rec,
!                                                       NULL, rec->erh);
!                 expanded_record_set_tuple(newerh, NULL, false);
!                 assign_record_var(estate, rec, newerh);
!             }
!             else
!             {
!                 /* Just clear it to NULL */
!                 if (rec->erh)
!                     DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
!                 rec->erh = NULL;
!             }
              return;
          }

diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
index f499d96..a2634c4 100644
*** a/src/pl/plpgsql/src/pl_funcs.c
--- b/src/pl/plpgsql/src/pl_funcs.c
*************** plpgsql_free_function_memory(PLpgSQL_fun
*** 718,723 ****
--- 718,728 ----
              case PLPGSQL_DTYPE_ROW:
                  break;
              case PLPGSQL_DTYPE_REC:
+                 {
+                     PLpgSQL_rec *rec = (PLpgSQL_rec *) d;
+
+                     free_expr(rec->default_val);
+                 }
                  break;
              case PLPGSQL_DTYPE_RECFIELD:
                  break;
*************** plpgsql_dumptree(PLpgSQL_function *func)
*** 1589,1594 ****
--- 1594,1609 ----
                  printf("REC %-16s typoid %u\n",
                         ((PLpgSQL_rec *) d)->refname,
                         ((PLpgSQL_rec *) d)->rectypeid);
+                 if (((PLpgSQL_rec *) d)->isconst)
+                     printf("                                  CONSTANT\n");
+                 if (((PLpgSQL_rec *) d)->notnull)
+                     printf("                                  NOT NULL\n");
+                 if (((PLpgSQL_rec *) d)->default_val != NULL)
+                 {
+                     printf("                                  DEFAULT ");
+                     dump_expr(((PLpgSQL_rec *) d)->default_val);
+                     printf("\n");
+                 }
                  break;
              case PLPGSQL_DTYPE_RECFIELD:
                  printf("RECFIELD %-16s of REC %d\n",
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index df88ba3..e37f548 100644
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
*************** decl_statement    : decl_varname decl_const
*** 502,538 ****

                          var = plpgsql_build_variable($1.name, $1.lineno,
                                                       $3, true);
!                         if ($2)
!                         {
!                             if (var->dtype == PLPGSQL_DTYPE_VAR)
!                                 ((PLpgSQL_var *) var)->isconst = $2;
!                             else
!                                 ereport(ERROR,
!                                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                                          errmsg("record variable cannot be CONSTANT"),
!                                          parser_errposition(@2)));
!                         }
!                         if ($5)
!                         {
!                             if (var->dtype == PLPGSQL_DTYPE_VAR)
!                                 ((PLpgSQL_var *) var)->notnull = $5;
!                             else
!                                 ereport(ERROR,
!                                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                                          errmsg("record variable cannot be NOT NULL"),
!                                          parser_errposition(@4)));

!                         }
!                         if ($6 != NULL)
!                         {
!                             if (var->dtype == PLPGSQL_DTYPE_VAR)
!                                 ((PLpgSQL_var *) var)->default_val = $6;
!                             else
!                                 ereport(ERROR,
!                                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
!                                          errmsg("default value for record variable is not supported"),
!                                          parser_errposition(@5)));
!                         }
                      }
                  | decl_varname K_ALIAS K_FOR decl_aliasitem ';'
                      {
--- 502,521 ----

                          var = plpgsql_build_variable($1.name, $1.lineno,
                                                       $3, true);
!                         var->isconst = $2;
!                         var->notnull = $5;
!                         var->default_val = $6;

!                         /*
!                          * The combination of NOT NULL without an initializer
!                          * can't work, so let's reject it at compile time.
!                          */
!                         if (var->notnull && var->default_val == NULL)
!                             ereport(ERROR,
!                                     (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
!                                      errmsg("variable \"%s\" must have a default value, since it's declared NOT
NULL",
!                                             var->refname),
!                                      parser_errposition(@5)));
                      }
                  | decl_varname K_ALIAS K_FOR decl_aliasitem ';'
                      {
*************** decl_cursor_args :
*** 632,637 ****
--- 615,621 ----
                          foreach (l, $2)
                          {
                              PLpgSQL_variable *arg = (PLpgSQL_variable *) lfirst(l);
+                             Assert(!arg->isconst);
                              new->fieldnames[i] = arg->refname;
                              new->varnos[i] = arg->dno;
                              i++;
*************** for_control        : for_variable K_IN
*** 1378,1383 ****
--- 1362,1368 ----
                              new->var = (PLpgSQL_variable *)
                                  plpgsql_build_record($1.name,
                                                       $1.lineno,
+                                                      NULL,
                                                       RECORDOID,
                                                       true);

*************** exception_sect    :
*** 2205,2211 ****
                                                                              -1,

plpgsql_curr_compile->fn_input_collation),
                                                       true);
!                         ((PLpgSQL_var *) var)->isconst = true;
                          new->sqlstate_varno = var->dno;

                          var = plpgsql_build_variable("sqlerrm", lineno,
--- 2190,2196 ----
                                                                              -1,

plpgsql_curr_compile->fn_input_collation),
                                                       true);
!                         var->isconst = true;
                          new->sqlstate_varno = var->dno;

                          var = plpgsql_build_variable("sqlerrm", lineno,
*************** exception_sect    :
*** 2213,2219 ****
                                                                              -1,

plpgsql_curr_compile->fn_input_collation),
                                                       true);
!                         ((PLpgSQL_var *) var)->isconst = true;
                          new->sqlerrm_varno = var->dno;

                          $<exception_block>$ = new;
--- 2198,2204 ----
                                                                              -1,

plpgsql_curr_compile->fn_input_collation),
                                                       true);
!                         var->isconst = true;
                          new->sqlerrm_varno = var->dno;

                          $<exception_block>$ = new;
*************** check_assignable(PLpgSQL_datum *datum, i
*** 3287,3310 ****
      {
          case PLPGSQL_DTYPE_VAR:
          case PLPGSQL_DTYPE_PROMISE:
!             if (((PLpgSQL_var *) datum)->isconst)
                  ereport(ERROR,
                          (errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
!                          errmsg("\"%s\" is declared CONSTANT",
!                                 ((PLpgSQL_var *) datum)->refname),
                           parser_errposition(location)));
              break;
          case PLPGSQL_DTYPE_ROW:
!             /* always assignable?  Shouldn't we check member vars? */
!             break;
!         case PLPGSQL_DTYPE_REC:
!             /* always assignable?  What about NEW/OLD? */
              break;
          case PLPGSQL_DTYPE_RECFIELD:
!             /* always assignable? */
              break;
          case PLPGSQL_DTYPE_ARRAYELEM:
!             /* always assignable? */
              break;
          default:
              elog(ERROR, "unrecognized dtype: %d", datum->dtype);
--- 3272,3297 ----
      {
          case PLPGSQL_DTYPE_VAR:
          case PLPGSQL_DTYPE_PROMISE:
!         case PLPGSQL_DTYPE_REC:
!             if (((PLpgSQL_variable *) datum)->isconst)
                  ereport(ERROR,
                          (errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
!                          errmsg("variable \"%s\" is declared CONSTANT",
!                                 ((PLpgSQL_variable *) datum)->refname),
                           parser_errposition(location)));
              break;
          case PLPGSQL_DTYPE_ROW:
!             /* always assignable; member vars were checked at compile time */
              break;
          case PLPGSQL_DTYPE_RECFIELD:
!             /* assignable if parent record is */
!             check_assignable(plpgsql_Datums[((PLpgSQL_recfield *) datum)->recparentno],
!                              location);
              break;
          case PLPGSQL_DTYPE_ARRAYELEM:
!             /* assignable if parent array is */
!             check_assignable(plpgsql_Datums[((PLpgSQL_arrayelem *) datum)->arrayparentno],
!                              location);
              break;
          default:
              elog(ERROR, "unrecognized dtype: %d", datum->dtype);
*************** read_into_scalar_list(char *initial_name
*** 3429,3437 ****
       */
      plpgsql_push_back_token(tok);

!     row = palloc(sizeof(PLpgSQL_row));
      row->dtype = PLPGSQL_DTYPE_ROW;
-     row->refname = pstrdup("*internal*");
      row->lineno = plpgsql_location_to_lineno(initial_location);
      row->rowtupdesc = NULL;
      row->nfields = nfields;
--- 3416,3423 ----
       */
      plpgsql_push_back_token(tok);

!     row = palloc0(sizeof(PLpgSQL_row));
      row->dtype = PLPGSQL_DTYPE_ROW;
      row->lineno = plpgsql_location_to_lineno(initial_location);
      row->rowtupdesc = NULL;
      row->nfields = nfields;
*************** make_scalar_list1(char *initial_name,
*** 3464,3472 ****

      check_assignable(initial_datum, location);

!     row = palloc(sizeof(PLpgSQL_row));
      row->dtype = PLPGSQL_DTYPE_ROW;
-     row->refname = pstrdup("*internal*");
      row->lineno = lineno;
      row->rowtupdesc = NULL;
      row->nfields = 1;
--- 3450,3457 ----

      check_assignable(initial_datum, location);

!     row = palloc0(sizeof(PLpgSQL_row));
      row->dtype = PLPGSQL_DTYPE_ROW;
      row->lineno = lineno;
      row->rowtupdesc = NULL;
      row->nfields = 1;
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index 96e92b2..c1c2b40 100644
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** typedef struct PLpgSQL_variable
*** 262,267 ****
--- 262,270 ----
      int            dno;
      char       *refname;
      int            lineno;
+     bool        isconst;
+     bool        notnull;
+     PLpgSQL_expr *default_val;
  } PLpgSQL_variable;

  /*
*************** typedef struct PLpgSQL_var
*** 281,292 ****
      int            dno;
      char       *refname;
      int            lineno;
-     /* end of PLpgSQL_variable fields */
-
      bool        isconst;
      bool        notnull;
-     PLpgSQL_type *datatype;
      PLpgSQL_expr *default_val;

      /*
       * Variables declared as CURSOR FOR <query> are mostly like ordinary
--- 284,295 ----
      int            dno;
      char       *refname;
      int            lineno;
      bool        isconst;
      bool        notnull;
      PLpgSQL_expr *default_val;
+     /* end of PLpgSQL_variable fields */
+
+     PLpgSQL_type *datatype;

      /*
       * Variables declared as CURSOR FOR <query> are mostly like ordinary
*************** typedef struct PLpgSQL_var
*** 318,323 ****
--- 321,331 ----
   *
   * Note that there's no way to name the row as such from PL/pgSQL code,
   * so many functions don't need to support these.
+  *
+  * refname, isconst, notnull, and default_val are unsupported (and hence
+  * always zero/null) for a row.  The member variables of a row should have
+  * been checked to be writable at compile time, so isconst is correctly set
+  * to false.  notnull and default_val aren't applicable.
   */
  typedef struct PLpgSQL_row
  {
*************** typedef struct PLpgSQL_row
*** 325,330 ****
--- 333,341 ----
      int            dno;
      char       *refname;
      int            lineno;
+     bool        isconst;
+     bool        notnull;
+     PLpgSQL_expr *default_val;
      /* end of PLpgSQL_variable fields */

      /*
*************** typedef struct PLpgSQL_rec
*** 348,358 ****
--- 359,376 ----
      int            dno;
      char       *refname;
      int            lineno;
+     bool        isconst;
+     bool        notnull;
+     PLpgSQL_expr *default_val;
      /* end of PLpgSQL_variable fields */

+     PLpgSQL_type *datatype;        /* can be NULL, if rectypeid is RECORDOID */
      Oid            rectypeid;        /* declared type of variable */
      /* RECFIELDs for this record are chained together for easy access */
      int            firstfield;        /* dno of first RECFIELD, or -1 if none */
+
+     /* Fields below here can change at runtime */
+
      /* We always store record variables as "expanded" records */
      ExpandedRecordHeader *erh;
  } PLpgSQL_rec;
*************** extern PLpgSQL_variable *plpgsql_build_v
*** 1121,1127 ****
                         PLpgSQL_type *dtype,
                         bool add2namespace);
  extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno,
!                      Oid rectypeid, bool add2namespace);
  extern PLpgSQL_recfield *plpgsql_build_recfield(PLpgSQL_rec *rec,
                         const char *fldname);
  extern int plpgsql_recognize_err_condition(const char *condname,
--- 1139,1146 ----
                         PLpgSQL_type *dtype,
                         bool add2namespace);
  extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno,
!                      PLpgSQL_type *dtype, Oid rectypeid,
!                      bool add2namespace);
  extern PLpgSQL_recfield *plpgsql_build_recfield(PLpgSQL_rec *rec,
                         const char *fldname);
  extern int plpgsql_recognize_err_condition(const char *condname,
diff --git a/src/pl/plpgsql/src/sql/plpgsql_varprops.sql b/src/pl/plpgsql/src/sql/plpgsql_varprops.sql
index ...c0e7f95 .
*** a/src/pl/plpgsql/src/sql/plpgsql_varprops.sql
--- b/src/pl/plpgsql/src/sql/plpgsql_varprops.sql
***************
*** 0 ****
--- 1,249 ----
+ --
+ -- Tests for PL/pgSQL variable properties: CONSTANT, NOT NULL, initializers
+ --
+
+ create type var_record as (f1 int4, f2 int4);
+ create domain int_nn as int not null;
+ create domain var_record_nn as var_record not null;
+ create domain var_record_colnn as var_record check((value).f2 is not null);
+
+ -- CONSTANT
+
+ do $$
+ declare x constant int := 42;
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x constant int;
+ begin
+   x := 42;  -- fail
+ end$$;
+
+ do $$
+ declare x constant int; y int;
+ begin
+   for x, y in select 1, 2 loop  -- fail
+   end loop;
+ end$$;
+
+ do $$
+ declare x constant int[];
+ begin
+   x[1] := 42;  -- fail
+ end$$;
+
+ do $$
+ declare x constant int[]; y int;
+ begin
+   for x[1], y in select 1, 2 loop  -- fail (currently, unsupported syntax)
+   end loop;
+ end$$;
+
+ do $$
+ declare x constant var_record;
+ begin
+   x.f1 := 42;  -- fail
+ end$$;
+
+ do $$
+ declare x constant var_record; y int;
+ begin
+   for x.f1, y in select 1, 2 loop  -- fail
+   end loop;
+ end$$;
+
+ -- initializer expressions
+
+ do $$
+ declare x int := sin(0);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x int := 1/0;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x bigint[] := array[1,3,5];
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x record := row(1,2,3);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record := row(1,2);
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ -- NOT NULL
+
+ do $$
+ declare x int not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x int not null := 42;
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x int not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x record not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x record not null := row(42);
+ begin
+   raise notice 'x = %', x;
+   x := row(null);  -- ok
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x record not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record not null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record not null := row(41,42);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- ok
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x var_record not null := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ -- Check that variables are reinitialized on block re-entry.
+
+ \set VERBOSITY terse   \\ -- needed for output stability
+ do $$
+ begin
+   for i in 1..3 loop
+     declare
+       x int;
+       y int := i;
+       r record;
+       c var_record;
+     begin
+       if i = 1 then
+         x := 42;
+         r := row(i, i+1);
+         c := row(i, i+1);
+       end if;
+       raise notice 'x = %', x;
+       raise notice 'y = %', y;
+       raise notice 'r = %', r;
+       raise notice 'c = %', c;
+     end;
+   end loop;
+ end$$;
+ \set VERBOSITY default
+
+ -- Check enforcement of domain constraints during initialization
+
+ do $$
+ declare x int_nn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x int_nn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x int_nn := 42;
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x var_record_nn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record_nn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record_nn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- ok
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x var_record_colnn;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record_colnn := null;  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record_colnn := row(1,null);  -- fail
+ begin
+   raise notice 'x = %', x;
+ end$$;
+
+ do $$
+ declare x var_record_colnn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := null;  -- fail
+ end$$;
+
+ do $$
+ declare x var_record_colnn := row(1,2);
+ begin
+   raise notice 'x = %', x;
+   x := row(null,null);  -- fail
+ end$$;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index e38fe90..bb35326 100644
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** select scope_test();
*** 5025,5066 ****
  (1 row)

  drop function scope_test();
- -- Check that variables are reinitialized on block re-entry.
- \set VERBOSITY terse   \\ -- needed for output stability
- do $$
- begin
-   for i in 1..3 loop
-     declare
-       x int;
-       y int := i;
-       r record;
-       c int8_tbl;
-     begin
-       if i = 1 then
-         x := 42;
-         r := row(i, i+1);
-         c := row(i, i+1);
-       end if;
-       raise notice 'x = %', x;
-       raise notice 'y = %', y;
-       raise notice 'r = %', r;
-       raise notice 'c = %', c;
-     end;
-   end loop;
- end$$;
- NOTICE:  x = 42
- NOTICE:  y = 1
- NOTICE:  r = (1,2)
- NOTICE:  c = (1,2)
- NOTICE:  x = <NULL>
- NOTICE:  y = 2
- NOTICE:  r = <NULL>
- NOTICE:  c = <NULL>
- NOTICE:  x = <NULL>
- NOTICE:  y = 3
- NOTICE:  r = <NULL>
- NOTICE:  c = <NULL>
- \set VERBOSITY default
  -- Check handling of conflicts between plpgsql vars and table columns.
  set plpgsql.variable_conflict = error;
  create function conflict_test() returns setof int8_tbl as $$
--- 5025,5030 ----
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 132f912..6620ea6 100644
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** select scope_test();
*** 4014,4045 ****

  drop function scope_test();

- -- Check that variables are reinitialized on block re-entry.
-
- \set VERBOSITY terse   \\ -- needed for output stability
- do $$
- begin
-   for i in 1..3 loop
-     declare
-       x int;
-       y int := i;
-       r record;
-       c int8_tbl;
-     begin
-       if i = 1 then
-         x := 42;
-         r := row(i, i+1);
-         c := row(i, i+1);
-       end if;
-       raise notice 'x = %', x;
-       raise notice 'y = %', y;
-       raise notice 'r = %', r;
-       raise notice 'c = %', c;
-     end;
-   end loop;
- end$$;
- \set VERBOSITY default
-
  -- Check handling of conflicts between plpgsql vars and table columns.

  set plpgsql.variable_conflict = error;
--- 4014,4019 ----

pgsql-hackers by date:

Previous
From: Tom Lane
Date:
Subject: Re: Converting plpgsql to use DTYPE_REC for named composite types
Next
From: Craig Ringer
Date:
Subject: Re: [PATCH] session_replication_role = replica with TRUNCATE