I wrote: > 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.
Here's a version rebased up to HEAD, with a trivial merge conflict fixed. This now needs to be applied over the patches in https://postgr.es/m/833.1516834...@sss.pgh.pa.us and https://postgr.es/m/8376.1516835...@sss.pgh.pa.us regards, tom lane
diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 2190eab..3ac64e2 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_control plpgsql_record plpgsql_transaction all: all-lib --- 26,33 ---- REGRESS_OPTS = --dbname=$(PL_TESTDB) ! 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 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 526aa8f..aab92c4 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 d578c56..62010d8 100644 *** a/src/pl/plpgsql/src/pl_exec.c --- b/src/pl/plpgsql/src/pl_exec.c *************** plpgsql_exec_function(PLpgSQL_function * *** 539,545 **** } else { ! /* If arg is null, treat it as an empty row */ exec_move_row(&estate, (PLpgSQL_variable *) rec, NULL, NULL); } --- 539,545 ---- } else { ! /* If arg is null, set variable to null */ exec_move_row(&estate, (PLpgSQL_variable *) rec, NULL, NULL); } *************** exec_stmt_block(PLpgSQL_execstate *estat *** 1539,1549 **** { /* * 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, --- 1539,1547 ---- { /* * 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 *** 1553,1563 **** 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 { --- 1551,1558 ---- UNKNOWNOID, -1); ! /* parser should have rejected NOT NULL */ ! Assert(!var->notnull); } else { *************** exec_stmt_block(PLpgSQL_execstate *estat *** 1571,1579 **** { PLpgSQL_rec *rec = (PLpgSQL_rec *) datum; ! if (rec->erh) ! DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh)); ! rec->erh = NULL; } break; --- 1566,1593 ---- { 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 *** 4725,4731 **** if (isNull) { ! /* If source is null, just assign nulls to the record */ exec_move_row(estate, (PLpgSQL_variable *) rec, NULL, NULL); } --- 4739,4751 ---- 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, *** 6375,6383 **** */ if (tupdesc == NULL) { ! if (rec->erh) ! DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh)); ! rec->erh = NULL; return; } --- 6395,6421 ---- */ 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 379fd69..b986fc3 100644 *** a/src/pl/plpgsql/src/pl_funcs.c --- b/src/pl/plpgsql/src/pl_funcs.c *************** plpgsql_free_function_memory(PLpgSQL_fun *** 740,745 **** --- 740,750 ---- 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) *** 1633,1638 **** --- 1638,1653 ---- 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 5bf4594..688fbd6 100644 *** a/src/pl/plpgsql/src/pl_gram.y --- b/src/pl/plpgsql/src/pl_gram.y *************** decl_statement : decl_varname decl_const *** 505,541 **** 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 ';' { --- 505,524 ---- 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 : *** 635,640 **** --- 618,624 ---- 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 *** 1385,1390 **** --- 1369,1375 ---- new->var = (PLpgSQL_variable *) plpgsql_build_record($1.name, $1.lineno, + NULL, RECORDOID, true); *************** exception_sect : *** 2237,2243 **** -1, plpgsql_curr_compile->fn_input_collation), true); ! ((PLpgSQL_var *) var)->isconst = true; new->sqlstate_varno = var->dno; var = plpgsql_build_variable("sqlerrm", lineno, --- 2222,2228 ---- -1, plpgsql_curr_compile->fn_input_collation), true); ! var->isconst = true; new->sqlstate_varno = var->dno; var = plpgsql_build_variable("sqlerrm", lineno, *************** exception_sect : *** 2245,2251 **** -1, plpgsql_curr_compile->fn_input_collation), true); ! ((PLpgSQL_var *) var)->isconst = true; new->sqlerrm_varno = var->dno; $<exception_block>$ = new; --- 2230,2236 ---- -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 *** 3321,3344 **** { 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); --- 3306,3331 ---- { 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 *** 3463,3471 **** */ 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; --- 3450,3457 ---- */ 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, *** 3498,3506 **** 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; --- 3484,3491 ---- 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 01b89a5..c2449f0 100644 *** a/src/pl/plpgsql/src/plpgsql.h --- b/src/pl/plpgsql/src/plpgsql.h *************** typedef struct PLpgSQL_variable *** 264,269 **** --- 264,272 ---- int dno; char *refname; int lineno; + bool isconst; + bool notnull; + PLpgSQL_expr *default_val; } PLpgSQL_variable; /* *************** typedef struct PLpgSQL_var *** 283,294 **** 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 --- 286,297 ---- 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 *** 320,325 **** --- 323,333 ---- * * 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 *** 327,332 **** --- 335,343 ---- int dno; char *refname; int lineno; + bool isconst; + bool notnull; + PLpgSQL_expr *default_val; /* end of PLpgSQL_variable fields */ /* *************** typedef struct PLpgSQL_rec *** 350,360 **** --- 361,378 ---- 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 *** 1141,1147 **** 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, --- 1159,1166 ---- 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 0c1da08..d294e53 100644 *** a/src/test/regress/expected/plpgsql.out --- b/src/test/regress/expected/plpgsql.out *************** select scope_test(); *** 4586,4627 **** (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 $$ --- 4586,4591 ---- diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 6bdcfe7..f17cf0b 100644 *** a/src/test/regress/sql/plpgsql.sql --- b/src/test/regress/sql/plpgsql.sql *************** select scope_test(); *** 3735,3766 **** 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; --- 3735,3740 ----