From: Tom Lane Date: Mon, 1 Dec 2008 01:30:18 +0000 (+0000) Subject: Refactor crosstab() to build and return a tuplestore instead of using X-Git-Tag: REL8_4_BETA1~605 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=cf407f164ea9246ba37866dda6e43f94d65e86f5;p=postgresql Refactor crosstab() to build and return a tuplestore instead of using value-per-call mode. This should be more efficient in normal usage, but the real problem with the prior coding was that it returned with a SPI call still active. That could cause problems if execution was interleaved with anything else that might use SPI. --- diff --git a/contrib/tablefunc/tablefunc.c b/contrib/tablefunc/tablefunc.c index 28b7a64f43..c6e452fc40 100644 --- a/contrib/tablefunc/tablefunc.c +++ b/contrib/tablefunc/tablefunc.c @@ -1,5 +1,5 @@ /* - * $PostgreSQL: pgsql/contrib/tablefunc/tablefunc.c,v 1.56 2008/11/30 23:23:52 tgl Exp $ + * $PostgreSQL: pgsql/contrib/tablefunc/tablefunc.c,v 1.57 2008/12/01 01:30:18 tgl Exp $ * * * tablefunc @@ -94,12 +94,6 @@ typedef struct bool use_carry; /* use second generated value */ } normal_rand_fctx; -typedef struct -{ - SPITupleTable *spi_tuptable; /* sql results from user query */ - char *lastrowid; /* rowid of the last tuple sent */ -} crosstab_fctx; - #define xpfree(var_) \ do { \ if (var_ != NULL) \ @@ -356,304 +350,254 @@ PG_FUNCTION_INFO_V1(crosstab); Datum crosstab(PG_FUNCTION_ARGS) { - FuncCallContext *funcctx; - TupleDesc ret_tupdesc; + char *sql = text_to_cstring(PG_GETARG_TEXT_PP(0)); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Tuplestorestate *tupstore; + TupleDesc tupdesc; int call_cntr; int max_calls; AttInMetadata *attinmeta; - SPITupleTable *spi_tuptable = NULL; + SPITupleTable *spi_tuptable; TupleDesc spi_tupdesc; - char *lastrowid = NULL; - crosstab_fctx *fctx; + bool firstpass; + char *lastrowid; int i; int num_categories; - bool firstpass = false; + MemoryContext per_query_ctx; MemoryContext oldcontext; + int ret; + int proc; - /* stuff done only on the first call of the function */ - if (SRF_IS_FIRSTCALL()) - { - char *sql = text_to_cstring(PG_GETARG_TEXT_PP(0)); - TupleDesc tupdesc; - int ret; - int proc; - - /* create a function context for cross-call persistence */ - funcctx = SRF_FIRSTCALL_INIT(); + /* check to see if caller supports us returning a tuplestore */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not " \ + "allowed in this context"))); - /* Connect to SPI manager */ - if ((ret = SPI_connect()) < 0) - /* internal error */ - elog(ERROR, "crosstab: SPI_connect returned %d", ret); + per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; - /* Retrieve the desired rows */ - ret = SPI_execute(sql, true, 0); - proc = SPI_processed; + /* Connect to SPI manager */ + if ((ret = SPI_connect()) < 0) + /* internal error */ + elog(ERROR, "crosstab: SPI_connect returned %d", ret); - /* Check for qualifying tuples */ - if ((ret == SPI_OK_SELECT) && (proc > 0)) - { - spi_tuptable = SPI_tuptable; - spi_tupdesc = spi_tuptable->tupdesc; - - /*---------- - * The provided SQL query must always return three columns. - * - * 1. rowname - * the label or identifier for each row in the final result - * 2. category - * the label or identifier for each column in the final result - * 3. values - * the value for each column in the final result - *---------- - */ - if (spi_tupdesc->natts != 3) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid source data SQL statement"), - errdetail("The provided SQL must return 3 " - "columns: rowid, category, and values."))); - } - else - { - /* no qualifying tuples */ - SPI_finish(); - SRF_RETURN_DONE(funcctx); - } + /* Retrieve the desired rows */ + ret = SPI_execute(sql, true, 0); + proc = SPI_processed; - /* get a tuple descriptor for our result type */ - switch (get_call_result_type(fcinfo, NULL, &tupdesc)) - { - case TYPEFUNC_COMPOSITE: - /* success */ - break; - case TYPEFUNC_RECORD: - /* failed to determine actual type of RECORD */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("function returning record called in context " - "that cannot accept type record"))); - break; - default: - /* result type isn't composite */ - elog(ERROR, "return type must be a row type"); - break; - } + /* If no qualifying tuples, fall out early */ + if (ret != SPI_OK_SELECT || proc <= 0) + { + SPI_finish(); + rsinfo->isDone = ExprEndResult; + PG_RETURN_NULL(); + } - /* - * Check that return tupdesc is compatible with the data we got from - * SPI, at least based on number and type of attributes - */ - if (!compatCrosstabTupleDescs(tupdesc, spi_tupdesc)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("return and sql tuple descriptions are " \ - "incompatible"))); + spi_tuptable = SPI_tuptable; + spi_tupdesc = spi_tuptable->tupdesc; - /* - * switch to memory context appropriate for multiple function calls - */ - oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + /*---------- + * The provided SQL query must always return three columns. + * + * 1. rowname + * the label or identifier for each row in the final result + * 2. category + * the label or identifier for each column in the final result + * 3. values + * the value for each column in the final result + *---------- + */ + if (spi_tupdesc->natts != 3) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid source data SQL statement"), + errdetail("The provided SQL must return 3 " + "columns: rowid, category, and values."))); - /* make sure we have a persistent copy of the tupdesc */ - tupdesc = CreateTupleDescCopy(tupdesc); + /* get a tuple descriptor for our result type */ + switch (get_call_result_type(fcinfo, NULL, &tupdesc)) + { + case TYPEFUNC_COMPOSITE: + /* success */ + break; + case TYPEFUNC_RECORD: + /* failed to determine actual type of RECORD */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + break; + default: + /* result type isn't composite */ + elog(ERROR, "return type must be a row type"); + break; + } - /* - * Generate attribute metadata needed later to produce tuples from raw - * C strings - */ - attinmeta = TupleDescGetAttInMetadata(tupdesc); - funcctx->attinmeta = attinmeta; + /* + * Check that return tupdesc is compatible with the data we got from + * SPI, at least based on number and type of attributes + */ + if (!compatCrosstabTupleDescs(tupdesc, spi_tupdesc)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("return and sql tuple descriptions are " \ + "incompatible"))); - /* allocate memory for user context */ - fctx = (crosstab_fctx *) palloc(sizeof(crosstab_fctx)); + /* + * switch to long-lived memory context + */ + oldcontext = MemoryContextSwitchTo(per_query_ctx); - /* - * Save spi data for use across calls - */ - fctx->spi_tuptable = spi_tuptable; - fctx->lastrowid = NULL; - funcctx->user_fctx = fctx; + /* make sure we have a persistent copy of the result tupdesc */ + tupdesc = CreateTupleDescCopy(tupdesc); - /* total number of tuples to be returned */ - funcctx->max_calls = proc; + /* initialize our tuplestore in long-lived context */ + tupstore = + tuplestore_begin_heap(rsinfo->allowedModes & SFRM_Materialize_Random, + false, work_mem); - MemoryContextSwitchTo(oldcontext); - firstpass = true; - } - - /* stuff done on every call of the function */ - funcctx = SRF_PERCALL_SETUP(); + MemoryContextSwitchTo(oldcontext); /* - * initialize per-call variables + * Generate attribute metadata needed later to produce tuples from raw + * C strings */ - call_cntr = funcctx->call_cntr; - max_calls = funcctx->max_calls; - - /* user context info */ - fctx = (crosstab_fctx *) funcctx->user_fctx; - lastrowid = fctx->lastrowid; - spi_tuptable = fctx->spi_tuptable; - - /* the sql tuple */ - spi_tupdesc = spi_tuptable->tupdesc; + attinmeta = TupleDescGetAttInMetadata(tupdesc); - /* attribute return type and return tuple description */ - attinmeta = funcctx->attinmeta; - ret_tupdesc = attinmeta->tupdesc; + /* total number of tuples to be examined */ + max_calls = proc; /* the return tuple always must have 1 rowid + num_categories columns */ - num_categories = ret_tupdesc->natts - 1; + num_categories = tupdesc->natts - 1; - if (call_cntr < max_calls) /* do when there is more left to send */ + firstpass = true; + lastrowid = NULL; + + for (call_cntr = 0; call_cntr < max_calls; call_cntr++) { - HeapTuple tuple; - Datum result; - char **values; bool skip_tuple = false; + char **values; + + /* allocate and zero space */ + values = (char **) palloc0((1 + num_categories) * sizeof(char *)); - while (true) + /* + * now loop through the sql results and assign each value in + * sequence to the next category + */ + for (i = 0; i < num_categories; i++) { - /* allocate space */ - values = (char **) palloc((1 + num_categories) * sizeof(char *)); + HeapTuple spi_tuple; + char *rowid; + + /* see if we've gone too far already */ + if (call_cntr >= max_calls) + break; - /* and make sure it's clear */ - memset(values, '\0', (1 + num_categories) * sizeof(char *)); + /* get the next sql result tuple */ + spi_tuple = spi_tuptable->vals[call_cntr]; + + /* get the rowid from the current sql result tuple */ + rowid = SPI_getvalue(spi_tuple, spi_tupdesc, 1); /* - * now loop through the sql results and assign each value in - * sequence to the next category + * If this is the first pass through the values for this + * rowid, set the first column to rowid */ - for (i = 0; i < num_categories; i++) + if (i == 0) { - HeapTuple spi_tuple; - char *rowid = NULL; - - /* see if we've gone too far already */ - if (call_cntr >= max_calls) - break; - - /* get the next sql result tuple */ - spi_tuple = spi_tuptable->vals[call_cntr]; - - /* get the rowid from the current sql result tuple */ - rowid = SPI_getvalue(spi_tuple, spi_tupdesc, 1); - - /* - * If this is the first pass through the values for this - * rowid, set the first column to rowid - */ - if (i == 0) - { - xpstrdup(values[0], rowid); - - /* - * Check to see if the rowid is the same as that of the - * last tuple sent -- if so, skip this tuple entirely - */ - if (!firstpass && xstreq(lastrowid, rowid)) - { - skip_tuple = true; - break; - } - } + xpstrdup(values[0], rowid); /* - * If rowid hasn't changed on us, continue building the ouput - * tuple. + * Check to see if the rowid is the same as that of the + * last tuple sent -- if so, skip this tuple entirely */ - if (xstreq(rowid, values[0])) - { - /* - * Get the next category item value, which is always - * attribute number three. - * - * Be careful to assign the value to the array index based - * on which category we are presently processing. - */ - values[1 + i] = SPI_getvalue(spi_tuple, spi_tupdesc, 3); - - /* - * increment the counter since we consume a row for each - * category, but not for last pass because the API will do - * that for us - */ - if (i < (num_categories - 1)) - call_cntr = ++funcctx->call_cntr; - } - else + if (!firstpass && xstreq(lastrowid, rowid)) { - /* - * We'll fill in NULLs for the missing values, but we need - * to decrement the counter since this sql result row - * doesn't belong to the current output tuple. - */ - call_cntr = --funcctx->call_cntr; + xpfree(rowid); + skip_tuple = true; break; } - xpfree(rowid); } /* - * switch to memory context appropriate for multiple function - * calls + * If rowid hasn't changed on us, continue building the output + * tuple. */ - oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - - xpfree(fctx->lastrowid); - xpstrdup(fctx->lastrowid, values[0]); - lastrowid = fctx->lastrowid; - - MemoryContextSwitchTo(oldcontext); - - if (!skip_tuple) + if (xstreq(rowid, values[0])) { - /* build the tuple */ - tuple = BuildTupleFromCStrings(attinmeta, values); - - /* make the tuple into a datum */ - result = HeapTupleGetDatum(tuple); - - /* Clean up */ - for (i = 0; i < num_categories + 1; i++) - if (values[i] != NULL) - xpfree(values[i]); - xpfree(values); + /* + * Get the next category item value, which is always + * attribute number three. + * + * Be careful to assign the value to the array index based + * on which category we are presently processing. + */ + values[1 + i] = SPI_getvalue(spi_tuple, spi_tupdesc, 3); - SRF_RETURN_NEXT(funcctx, result); + /* + * increment the counter since we consume a row for each + * category, but not for last pass because the outer loop + * will do that for us + */ + if (i < (num_categories - 1)) + call_cntr++; + xpfree(rowid); } else { /* - * Skipping this tuple entirely, but we need to advance the - * counter like the API would if we had returned one. + * We'll fill in NULLs for the missing values, but we need + * to decrement the counter since this sql result row + * doesn't belong to the current output tuple. */ - call_cntr = ++funcctx->call_cntr; + call_cntr--; + xpfree(rowid); + break; + } + } - /* we'll start over at the top */ - xpfree(values); + if (!skip_tuple) + { + HeapTuple tuple; - /* see if we've gone too far already */ - if (call_cntr >= max_calls) - { - /* release SPI related resources */ - SPI_finish(); - SRF_RETURN_DONE(funcctx); - } + /* build the tuple */ + tuple = BuildTupleFromCStrings(attinmeta, values); - /* need to reset this before the next tuple is started */ - skip_tuple = false; - } + /* switch to appropriate context while storing the tuple */ + oldcontext = MemoryContextSwitchTo(per_query_ctx); + tuplestore_puttuple(tupstore, tuple); + MemoryContextSwitchTo(oldcontext); + + heap_freetuple(tuple); } + + /* Remember current rowid */ + xpfree(lastrowid); + xpstrdup(lastrowid, values[0]); + firstpass = false; + + /* Clean up */ + for (i = 0; i < num_categories + 1; i++) + if (values[i] != NULL) + pfree(values[i]); + pfree(values); } - else - /* do when there is no more left */ - { - /* release SPI related resources */ - SPI_finish(); - SRF_RETURN_DONE(funcctx); - } + + /* let the caller know we're sending back a tuplestore */ + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + /* release SPI related resources (and return to caller's context) */ + SPI_finish(); + + return (Datum) 0; } /* @@ -1613,6 +1557,10 @@ compatCrosstabTupleDescs(TupleDesc ret_tupdesc, TupleDesc sql_tupdesc) Form_pg_attribute sql_attr; Oid sql_atttypid; + if (ret_tupdesc->natts < 2 || + sql_tupdesc->natts < 3) + return false; + /* check the rowid types match */ ret_atttypid = ret_tupdesc->attrs[0]->atttypid; sql_atttypid = sql_tupdesc->attrs[0]->atttypid;