]> granicus.if.org Git - postgresql/commitdiff
Support CONSTANT/NOT NULL/initial value for plpgsql composite variables.
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 14 Feb 2018 03:15:08 +0000 (22:15 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 14 Feb 2018 03:15:08 +0000 (22:15 -0500)
These features were never implemented previously for composite or record
variables ... not that the documentation admitted it, so there's no doc
updates here.

This also fixes some issues concerning enforcing DOMAIN NOT NULL
constraints against plpgsql variables, although I'm not sure that
that topic is completely dealt with.

I created a new plpgsql test file for these features, and moved the
one relevant existing test case into that file.

Tom Lane, reviewed by Daniel Gustafsson

Discussion: https://postgr.es/m/18362.1514605650@sss.pgh.pa.us

src/pl/plpgsql/src/Makefile
src/pl/plpgsql/src/expected/plpgsql_varprops.out [new file with mode: 0644]
src/pl/plpgsql/src/pl_comp.c
src/pl/plpgsql/src/pl_exec.c
src/pl/plpgsql/src/pl_funcs.c
src/pl/plpgsql/src/pl_gram.y
src/pl/plpgsql/src/plpgsql.h
src/pl/plpgsql/src/sql/plpgsql_varprops.sql [new file with mode: 0644]
src/test/regress/expected/plpgsql.out
src/test/regress/sql/plpgsql.sql

index 2190eab616941b19d9b4813f3420478c22464c2f..3ac64e2d4478f89ea7c21a82194b243550698040 100644 (file)
@@ -26,7 +26,8 @@ DATA = plpgsql.control plpgsql--1.0.sql plpgsql--unpackaged--1.0.sql
 
 REGRESS_OPTS = --dbname=$(PL_TESTDB)
 
-REGRESS = plpgsql_call plpgsql_control plpgsql_record plpgsql_transaction
+REGRESS = plpgsql_call plpgsql_control plpgsql_record \
+       plpgsql_transaction 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
new file mode 100644 (file)
index 0000000..109056c
--- /dev/null
@@ -0,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
index 526aa8f6219db9147d4ca6fe83f618457bd9328f..aab92c4711c5de8385d749c84d4ae94c86c7ddff 100644 (file)
@@ -594,11 +594,11 @@ do_compile(FunctionCallInfo fcinfo,
                                                 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);
+                       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, RECORDOID, true);
+                       rec = plpgsql_build_record("old", 0, NULL, RECORDOID, true);
                        function->old_varno = rec->dno;
 
                        /* Add the variable tg_name */
@@ -1811,7 +1811,7 @@ plpgsql_build_variable(const char *refname, int lineno, PLpgSQL_type *dtype,
                                var->refname = pstrdup(refname);
                                var->lineno = lineno;
                                var->datatype = dtype;
-                               /* other fields might be filled by caller */
+                               /* other fields are left as 0, might be changed by caller */
 
                                /* preset to NULL */
                                var->value = 0;
@@ -1831,7 +1831,8 @@ plpgsql_build_variable(const char *refname, int lineno, PLpgSQL_type *dtype,
                                /* Composite type -- build a record variable */
                                PLpgSQL_rec *rec;
 
-                               rec = plpgsql_build_record(refname, lineno, dtype->typoid,
+                               rec = plpgsql_build_record(refname, lineno,
+                                                                                  dtype, dtype->typoid,
                                                                                   add2namespace);
                                result = (PLpgSQL_variable *) rec;
                                break;
@@ -1856,7 +1857,8 @@ plpgsql_build_variable(const char *refname, int lineno, PLpgSQL_type *dtype,
  * Build empty named record variable, and optionally add it to namespace
  */
 PLpgSQL_rec *
-plpgsql_build_record(const char *refname, int lineno, Oid rectypeid,
+plpgsql_build_record(const char *refname, int lineno,
+                                        PLpgSQL_type *dtype, Oid rectypeid,
                                         bool add2namespace)
 {
        PLpgSQL_rec *rec;
@@ -1865,6 +1867,8 @@ plpgsql_build_record(const char *refname, int lineno, Oid rectypeid,
        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;
@@ -1899,6 +1903,9 @@ build_row_from_vars(PLpgSQL_variable **vars, int numvars)
                int32           typmod;
                Oid                     typcoll;
 
+               /* Member vars of a row should never be const */
+               Assert(!var->isconst);
+
                switch (var->dtype)
                {
                        case PLPGSQL_DTYPE_VAR:
index f6866743ac1e7057f9daff5fce190820edbf1065..5054d20ab152f8285509530679897b13542df05c 100644 (file)
@@ -539,7 +539,7 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
                                        }
                                        else
                                        {
-                                               /* If arg is null, treat it as an empty row */
+                                               /* If arg is null, set variable to null */
                                                exec_move_row(&estate, (PLpgSQL_variable *) rec,
                                                                          NULL, NULL);
                                        }
@@ -1539,11 +1539,9 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
                                        {
                                                /*
                                                 * If needed, give the datatype a chance to reject
-                                                * NULLs, by assigning a NULL to the variable. We
+                                                * 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.)
+                                                * datatype, else coercion will be skipped.
                                                 */
                                                if (var->datatype->typtype == TYPTYPE_DOMAIN)
                                                        exec_assign_value(estate,
@@ -1553,11 +1551,8 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
                                                                                          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)));
+                                               /* parser should have rejected NOT NULL */
+                                               Assert(!var->notnull);
                                        }
                                        else
                                        {
@@ -1571,9 +1566,28 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
                                {
                                        PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
 
-                                       if (rec->erh)
-                                               DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
-                                       rec->erh = NULL;
+                                       /*
+                                        * 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;
 
@@ -4725,7 +4739,13 @@ exec_assign_value(PLpgSQL_execstate *estate,
 
                                if (isNull)
                                {
-                                       /* If source is null, just assign nulls to the record */
+                                       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);
                                }
@@ -6375,9 +6395,27 @@ exec_move_row(PLpgSQL_execstate *estate,
                 */
                if (tupdesc == NULL)
                {
-                       if (rec->erh)
-                               DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
-                       rec->erh = 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;
                }
 
index 379fd69f44a2d9ccaf3eea4a57e57988b6caa917..b986fc39b3869ca9874a97446f7a75331261564b 100644 (file)
@@ -740,6 +740,11 @@ plpgsql_free_function_memory(PLpgSQL_function *func)
                        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;
@@ -1633,6 +1638,16 @@ plpgsql_dumptree(PLpgSQL_function *func)
                                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",
index 5bf45942a60ca0d3235c10f577d83f1f5f009bf0..688fbd6531e21bba9dc1a65a450b6a192a889a89 100644 (file)
@@ -505,37 +505,20 @@ decl_statement    : decl_varname decl_const decl_datatype decl_collate decl_notnull
 
                                                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)));
+                                               var->isconst = $2;
+                                               var->notnull = $5;
+                                               var->default_val = $6;
 
-                                               }
-                                               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)));
-                                               }
+                                               /*
+                                                * 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 ';'
                                        {
@@ -635,6 +618,7 @@ decl_cursor_args :
                                                foreach (l, $2)
                                                {
                                                        PLpgSQL_variable *arg = (PLpgSQL_variable *) lfirst(l);
+                                                       Assert(!arg->isconst);
                                                        new->fieldnames[i] = arg->refname;
                                                        new->varnos[i] = arg->dno;
                                                        i++;
@@ -1385,6 +1369,7 @@ for_control               : for_variable K_IN
                                                        new->var = (PLpgSQL_variable *)
                                                                plpgsql_build_record($1.name,
                                                                                                         $1.lineno,
+                                                                                                        NULL,
                                                                                                         RECORDOID,
                                                                                                         true);
 
@@ -2237,7 +2222,7 @@ exception_sect    :
                                                                                                                                                        -1,
                                                                                                                                                        plpgsql_curr_compile->fn_input_collation),
                                                                                                         true);
-                                               ((PLpgSQL_var *) var)->isconst = true;
+                                               var->isconst = true;
                                                new->sqlstate_varno = var->dno;
 
                                                var = plpgsql_build_variable("sqlerrm", lineno,
@@ -2245,7 +2230,7 @@ exception_sect    :
                                                                                                                                                        -1,
                                                                                                                                                        plpgsql_curr_compile->fn_input_collation),
                                                                                                         true);
-                                               ((PLpgSQL_var *) var)->isconst = true;
+                                               var->isconst = true;
                                                new->sqlerrm_varno = var->dno;
 
                                                $<exception_block>$ = new;
@@ -3321,24 +3306,26 @@ check_assignable(PLpgSQL_datum *datum, int location)
        {
                case PLPGSQL_DTYPE_VAR:
                case PLPGSQL_DTYPE_PROMISE:
-                       if (((PLpgSQL_var *) datum)->isconst)
+               case PLPGSQL_DTYPE_REC:
+                       if (((PLpgSQL_variable *) datum)->isconst)
                                ereport(ERROR,
                                                (errcode(ERRCODE_ERROR_IN_ASSIGNMENT),
-                                                errmsg("\"%s\" is declared CONSTANT",
-                                                               ((PLpgSQL_var *) datum)->refname),
+                                                errmsg("variable \"%s\" is declared CONSTANT",
+                                                               ((PLpgSQL_variable *) 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? */
+                       /* always assignable; member vars were checked at compile time */
                        break;
                case PLPGSQL_DTYPE_RECFIELD:
-                       /* always assignable? */
+                       /* assignable if parent record is */
+                       check_assignable(plpgsql_Datums[((PLpgSQL_recfield *) datum)->recparentno],
+                                                        location);
                        break;
                case PLPGSQL_DTYPE_ARRAYELEM:
-                       /* always assignable? */
+                       /* assignable if parent array is */
+                       check_assignable(plpgsql_Datums[((PLpgSQL_arrayelem *) datum)->arrayparentno],
+                                                        location);
                        break;
                default:
                        elog(ERROR, "unrecognized dtype: %d", datum->dtype);
@@ -3463,9 +3450,8 @@ read_into_scalar_list(char *initial_name,
         */
        plpgsql_push_back_token(tok);
 
-       row = palloc(sizeof(PLpgSQL_row));
+       row = palloc0(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;
@@ -3498,9 +3484,8 @@ make_scalar_list1(char *initial_name,
 
        check_assignable(initial_datum, location);
 
-       row = palloc(sizeof(PLpgSQL_row));
+       row = palloc0(sizeof(PLpgSQL_row));
        row->dtype = PLPGSQL_DTYPE_ROW;
-       row->refname = pstrdup("*internal*");
        row->lineno = lineno;
        row->rowtupdesc = NULL;
        row->nfields = 1;
index 01b89a5ffa39af761c53b77efa5131d1b5496dc6..c2449f03cf9ea1b7ab4159ac9727c95f99cef4fb 100644 (file)
@@ -264,6 +264,9 @@ typedef struct PLpgSQL_variable
        int                     dno;
        char       *refname;
        int                     lineno;
+       bool            isconst;
+       bool            notnull;
+       PLpgSQL_expr *default_val;
 } PLpgSQL_variable;
 
 /*
@@ -283,12 +286,12 @@ typedef struct PLpgSQL_var
        int                     dno;
        char       *refname;
        int                     lineno;
-       /* end of PLpgSQL_variable fields */
-
        bool            isconst;
        bool            notnull;
-       PLpgSQL_type *datatype;
        PLpgSQL_expr *default_val;
+       /* end of PLpgSQL_variable fields */
+
+       PLpgSQL_type *datatype;
 
        /*
         * Variables declared as CURSOR FOR <query> are mostly like ordinary
@@ -320,6 +323,11 @@ typedef struct PLpgSQL_var
  *
  * 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
 {
@@ -327,6 +335,9 @@ typedef struct PLpgSQL_row
        int                     dno;
        char       *refname;
        int                     lineno;
+       bool            isconst;
+       bool            notnull;
+       PLpgSQL_expr *default_val;
        /* end of PLpgSQL_variable fields */
 
        /*
@@ -350,11 +361,18 @@ typedef struct PLpgSQL_rec
        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;
@@ -1141,7 +1159,8 @@ extern PLpgSQL_variable *plpgsql_build_variable(const char *refname, int lineno,
                                           PLpgSQL_type *dtype,
                                           bool add2namespace);
 extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno,
-                                        Oid rectypeid, bool add2namespace);
+                                        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
new file mode 100644 (file)
index 0000000..c0e7f95
--- /dev/null
@@ -0,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$$;
index 0c1da088697f85816bd59ff9e6be23dd41f3f990..d294e5363455704e4097d692121321a83f783e06 100644 (file)
@@ -4586,42 +4586,6 @@ select scope_test();
 (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 $$
index 6bdcfe7cc585e7827b039b2e0103493a297344b3..f17cf0b49bf60ce4d457d760dd6b668d8cacd984 100644 (file)
@@ -3735,32 +3735,6 @@ select scope_test();
 
 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;