From: Tom Lane Date: Wed, 14 Feb 2018 03:15:08 +0000 (-0500) Subject: Support CONSTANT/NOT NULL/initial value for plpgsql composite variables. X-Git-Tag: REL_11_BETA1~783 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=f9263006d;p=postgresql Support CONSTANT/NOT NULL/initial value for plpgsql composite variables. 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 --- diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 2190eab616..3ac64e2d44 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -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 index 0000000000..109056c054 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_varprops.out @@ -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 = +NOTICE: y = 2 +NOTICE: r = +NOTICE: c = +NOTICE: x = +NOTICE: y = 3 +NOTICE: r = +NOTICE: c = +\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 526aa8f621..aab92c4711 100644 --- a/src/pl/plpgsql/src/pl_comp.c +++ b/src/pl/plpgsql/src/pl_comp.c @@ -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: diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index f6866743ac..5054d20ab1 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -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; } diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index 379fd69f44..b986fc39b3 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -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", diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 5bf45942a6..688fbd6531 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -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; $$ = 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; diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 01b89a5ffa..c2449f03cf 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -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 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 index 0000000000..c0e7f95f4e --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_varprops.sql @@ -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$$; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 0c1da08869..d294e53634 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -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 = -NOTICE: y = 2 -NOTICE: r = -NOTICE: c = -NOTICE: x = -NOTICE: y = 3 -NOTICE: r = -NOTICE: c = -\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 $$ diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 6bdcfe7cc5..f17cf0b49b 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -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;