Hi all, I've cleaned this up a bit so that assigning to a dynamic record field now works - ie NEW.(myvar) := 'someval' - and accessing a field by number works - ie myvar := OLD.(1)
It still doesn't work in a loop if the type of field referenced by the expression changes - looking at it more I'm really not sure this is possible, but that's a limitation I could live with. I'll also try figuring out freeing stuff after casting values over the weekend. But is this a worthwhile approach? If there are objections, please let me know! For myself, being able to pass a column name as an argument to a trigger would make writing generic triggers a whole lot easier. And going back through the archives on this list I see I'm not alone. Regards, Matt On Thu, 2004-11-18 at 13:18, Matt wrote: > Hi, > > I got extremely frustrated with having to create a temp table every time > I wanted to access an arbitrary column from a record plpgsql. After > seeing a note on the developers TODO about accessing plpgsql records > with a 'dot bracket' notation I started digging into the plpgsql source. > > My diff (against 8beta4) is attached. > > Warning: I Am Not a C Programmer! I haven't even written a hello world > in C before, and I knew nothing about Flex before yesterday. It was fun > figuring stuff out, I'm amazed it mostly works, but I'm really hoping > someone can point out my mistakes. > > Goal: > > Enable users to access fields in record variables using the following > syntax like the following: > rec.(1) > rec.('foo') > rec.(myvar::int) > rec.(myvar || '_id') > > Files changed: > > plpgsql.h > - added 'expr' member to PLpgSQL_recfield type for holding the > PLpgSQL_expr structure. > > scan.l > - added match for {identifier}{space}*\. AFAIK this should only match > if a longer expression doesn't? > > pl_comp.c > - added plpgsql_parse_wordexpr() function called by above match. Ripped > off code from plpgsql_parse_word that deals with arg_v[expr] to find our > expression. Prob a dumb name for the function! > > pl_exec.c > - hacked exec_assign_value() and exec_eval_datum() to use the expression > to get the field name/number. > > Stuff I need help with: > > 1. It should recognise OLD.(1) as a field number, not a column name. I > think I've got to check the returned type from exec_eval_expr() then > exec_simple_cast_value() on it, but that seems beyond me. > > 2. Freeing stuff. As I explained, this is all pretty new to me, and the > comments about it around exec_eval_expr() et al just confused me :( > Please give some hints about what needs freeing! > > 3. Would probably be good to add check in pl_comp.c to see if the > expression actually needs to be evaluated at runtime (ie isn't just a > field name/number). How? > > 4. Make this also work for row.(expr), label.record.(expr) and > label.row.(expr) - but want to get the basics working first! > > 5. Because of the way the expression is parsed (looking for closing > parenth), this will choke if you try and put a function in there. Would > it be better to use curly braces '{expr}' or another character to mark > the expression? > > I hope at this eventually leads to some really useful extra > functionality, particularly for writing generic triggers. And it's a > tribute to the existing code that a complete newbie can cut-and-paste > their way to a halfarsed solution in a (rather long) night! > > Regards, > > Matt > > ______________________________________________________________________ > > ---------------------------(end of broadcast)--------------------------- > TIP 9: the planner will ignore your desire to choose an index scan if your > joining column's datatypes do not match
diff -u src.orig/pl_comp.c src/pl_comp.c --- src.orig/pl_comp.c 2004-11-19 14:01:15.053237796 +0000 +++ src/pl_comp.c 2004-11-19 14:23:04.192899809 +0000 @@ -783,6 +783,75 @@ return T_WORD; } +/* ---------- + * plpgsql_parse_wordexpr Same lookup for word followed by dot. + * Should only get here if it wasn't followed by + * an identifier. + * ---------- + */ +int +plpgsql_parse_wordexpr(char *word) +{ + PLpgSQL_nsitem *ns; + char *cp[1]; + int save_spacescanned = plpgsql_SpaceScanned; + + /* Do case conversion and word separation */ + /* drop dot to keep converter happy */ + word[strlen(word) - 1] = '\0'; + plpgsql_convert_ident(word, cp, 1); + + /* Make sure we've got an open parenthesis */ + + /* + * Do a lookup on the compilers namestack + */ + ns = plpgsql_ns_lookup(cp[0], NULL); + if (ns == NULL) + { + pfree(cp[0]); + return T_ERROR; + } + switch (ns->itemtype) + { + case PLPGSQL_NSTYPE_REC: + { + /* + * First word is a record name, so expression refers to + * field in this record. + */ + PLpgSQL_recfield *new; + + new = malloc(sizeof(PLpgSQL_recfield)); + memset(new, 0, sizeof(PLpgSQL_recfield)); + + if (plpgsql_yylex() != '(') + plpgsql_yyerror("expected identifier or \"(\""); + new->expr = plpgsql_read_expression(')', ")"); + new->recparentno = ns->itemno; + /* just to be sure - we'll test on this later */ + new->fieldname = '\0'; + new->dtype = PLPGSQL_DTYPE_RECFIELD; + + plpgsql_adddatum((PLpgSQL_datum *) new); + + plpgsql_yylval.scalar = (PLpgSQL_datum *) new; + + plpgsql_SpaceScanned = save_spacescanned; + + pfree(cp[0]); + return T_SCALAR; + } + + /* TODO: deal with rows, too */ + + default: + break; + + } + pfree(cp[0]); + return T_ERROR; +} /* ---------- * plpgsql_parse_dblword Same lookup for two words diff -u src.orig/pl_exec.c src/pl_exec.c --- src.orig/pl_exec.c 2004-11-19 14:01:15.053237796 +0000 +++ src/pl_exec.c 2004-11-19 15:20:33.160897466 +0000 @@ -2965,7 +2965,8 @@ PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) target; PLpgSQL_rec *rec; int fno; - HeapTuple newtup; + char *fname; + HeapTuple newtup; int natts; int i; Datum *values; @@ -2974,7 +2975,7 @@ bool attisnull; Oid atttype; int32 atttypmod; - + rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]); /* @@ -2988,20 +2989,71 @@ errmsg("record \"%s\" is not assigned yet", rec->refname), errdetail("The tuple structure of a not-yet-assigned record is indeterminate."))); - - /* + + + /* Have we got an expr to deal with? */ + fno = 0; + fname = '\0'; + if (recfield->fieldname == '\0') + { + Datum exprdatum; + Oid exprtype; + bool *exprisnull; + + /* Evaluate expression */ + exprdatum = exec_eval_expr(estate, recfield->expr, + exprisnull, &exprtype); + + /* If we've got an integer, it's a field number, otherwise + * it's a fieldname + */ + if (exprtype == INT4OID) { + exprdatum = exec_simple_cast_value(exprdatum, + exprtype, + INT4OID, -1, + exprisnull); + fno = DatumGetInt32(exprdatum); + } + else { + fname = convert_value_to_string( + exprdatum, exprtype); + } + + /* do we need to free datum? */ + } + else { + fname = recfield->fieldname; + } + + + /* * Get the number of the records field to change and the - * number of attributes in the tuple. + * number of attributes in the tuple, if we didn't get it + * above. */ - fno = SPI_fnumber(rec->tupdesc, recfield->fieldname); - if (fno == SPI_ERROR_NOATTRIBUTE) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("record \"%s\" has no field \"%s\"", - rec->refname, recfield->fieldname))); - fno--; - natts = rec->tupdesc->natts; + if (fno < 1) + { + fno = SPI_fnumber(rec->tupdesc, fname); + if (fno == SPI_ERROR_NOATTRIBUTE) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("record \"%s\" has no field \"%s\"", + rec->refname, fname))); + } + fno--; + natts = rec->tupdesc->natts; + + /* + * Check that fno is within range + */ + if (fno >= natts) { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("record \"%s\" has no field number \"%d\"", + rec->refname, fno))); + } + /* * Set up values/datums arrays for heap_formtuple. For * all the attributes except the one we want to replace, @@ -3297,7 +3349,8 @@ PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) datum; PLpgSQL_rec *rec; int fno; - + char *fname; + rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]); if (!HeapTupleIsValid(rec->tup)) ereport(ERROR, @@ -3305,19 +3358,73 @@ errmsg("record \"%s\" is not assigned yet", rec->refname), errdetail("The tuple structure of a not-yet-assigned record is indeterminate."))); - fno = SPI_fnumber(rec->tupdesc, recfield->fieldname); - if (fno == SPI_ERROR_NOATTRIBUTE) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("record \"%s\" has no field \"%s\"", - rec->refname, recfield->fieldname))); - *typeid = SPI_gettypeid(rec->tupdesc, fno); + + /* Have we got an expr to deal with? */ + fno = 0; + fname = '\0'; + if (recfield->fieldname == '\0') + { + Datum exprdatum; + Oid exprtype; + bool *exprisnull; + + /* Evaluate expression */ + exprdatum = exec_eval_expr(estate, recfield->expr, + exprisnull, &exprtype); + + + /* If we've got an integer, it's a field number, otherwise + * it's a fieldname + */ + if (exprtype == INT4OID) { + exprdatum = exec_simple_cast_value(exprdatum, + exprtype, + INT4OID, -1, + exprisnull); + fno = DatumGetInt32(exprdatum); + } + else { + fname = convert_value_to_string( + exprdatum, exprtype); + } + } + else { + fname = recfield->fieldname; + } + + /* + * Get the number of the records field to change and the + * number of attributes in the tuple. + */ + if (fno < 1) + { + fno = SPI_fnumber(rec->tupdesc, fname); + if (fno == SPI_ERROR_NOATTRIBUTE) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("record \"%s\" has no field \"%s\"", + rec->refname, fname))); + } + + *typeid = SPI_gettypeid(rec->tupdesc, fno); *value = SPI_getbinval(rec->tup, rec->tupdesc, fno, isnull); - if (expectedtypeid != InvalidOid && expectedtypeid != *typeid) - ereport(ERROR, + if (*value == SPI_ERROR_NOATTRIBUTE) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("record \"%s\" has no field number \"%d\"", + rec->refname, fno))); + + if (expectedtypeid != InvalidOid && expectedtypeid != *typeid) + /* + * If an expression is used to determine the fieldname and + * the type of that field changes, we'll end up here. + * Should we cast it to the original type, or just throw + * an error? + */ + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("type of \"%s.%s\" does not match that when preparing the plan", - rec->refname, recfield->fieldname))); + rec->refname, fname))); break; } diff -u src.orig/plpgsql.h src/plpgsql.h --- src.orig/plpgsql.h 2004-11-19 14:01:15.054237644 +0000 +++ src/plpgsql.h 2004-11-19 14:23:17.751845320 +0000 @@ -269,6 +269,7 @@ int dtype; int rfno; char *fieldname; + PLpgSQL_expr *expr; int recparentno; /* dno of parent record */ } PLpgSQL_recfield; @@ -675,6 +676,7 @@ extern PLpgSQL_function *plpgsql_compile(FunctionCallInfo fcinfo, bool forValidator); extern int plpgsql_parse_word(char *word); +extern int plpgsql_parse_wordexpr(char *word); extern int plpgsql_parse_dblword(char *word); extern int plpgsql_parse_tripword(char *word); extern int plpgsql_parse_wordtype(char *word); diff -u src.orig/scan.l src/scan.l --- src.orig/scan.l 2004-11-19 14:01:15.054237644 +0000 +++ src/scan.l 2004-11-19 14:31:24.079117322 +0000 @@ -195,6 +195,9 @@ {identifier} { plpgsql_error_lineno = plpgsql_scanner_lineno(); return plpgsql_parse_word(yytext); } +{identifier}{space}*\. { + plpgsql_error_lineno = plpgsql_scanner_lineno(); + return plpgsql_parse_wordexpr(yytext); } {identifier}{space}*\.{space}*{identifier} { plpgsql_error_lineno = plpgsql_scanner_lineno(); return plpgsql_parse_dblword(yytext); }
---------------------------(end of broadcast)--------------------------- TIP 4: Don't 'kill -9' the postmaster