diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 2190eab6169..3ac64e2d447 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 00000000000..109056c0540 --- /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 526aa8f6219..aab92c4711c 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 f6866743ac1..5054d20ab15 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 379fd69f44a..b986fc39b38 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 5bf45942a60..688fbd6531e 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 01b89a5ffa3..c2449f03cf9 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 00000000000..c0e7f95f4e2 --- /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 0c1da088697..d294e536345 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 6bdcfe7cc58..f17cf0b49bf 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;