mirror of
https://github.com/postgres/postgres.git
synced 2025-07-30 11:03:19 +03:00
Fix coredump problem in plpgsql's RETURN NEXT. When a SELECT INTO
that's selecting into a RECORD variable returns zero rows, make it assign an all-nulls row to the RECORD; this is consistent with what happens when the SELECT INTO target is not a RECORD. In support of this, tweak the SPI code so that a valid tuple descriptor is returned even when a SPI select returns no rows.
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
$Header: /cvsroot/pgsql/doc/src/sgml/spi.sgml,v 1.24 2002/12/30 22:10:53 tgl Exp $
|
$Header: /cvsroot/pgsql/doc/src/sgml/spi.sgml,v 1.25 2003/01/21 22:06:11 tgl Exp $
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Chapter id="spi">
|
<Chapter id="spi">
|
||||||
@ -476,7 +476,7 @@ You may pass multiple queries in one string or query string may be
|
|||||||
The actual number of tuples for which the (last) query was executed is
|
The actual number of tuples for which the (last) query was executed is
|
||||||
returned in the global variable SPI_processed (if not <ReturnValue>SPI_OK_UTILITY</ReturnValue>).
|
returned in the global variable SPI_processed (if not <ReturnValue>SPI_OK_UTILITY</ReturnValue>).
|
||||||
|
|
||||||
If <ReturnValue>SPI_OK_SELECT</ReturnValue> is returned and SPI_processed > 0 then you may use global
|
If <ReturnValue>SPI_OK_SELECT</ReturnValue> is returned then you may use global
|
||||||
pointer SPITupleTable *SPI_tuptable to access the result tuples.
|
pointer SPITupleTable *SPI_tuptable to access the result tuples.
|
||||||
</Para>
|
</Para>
|
||||||
|
|
||||||
@ -517,7 +517,7 @@ You may pass multiple queries in one string or query string may be
|
|||||||
<TITLE>Structures
|
<TITLE>Structures
|
||||||
</TITLE>
|
</TITLE>
|
||||||
<Para>
|
<Para>
|
||||||
If <ReturnValue>SPI_OK_SELECT</ReturnValue> is returned and SPI_processed > 0 then you may use the global
|
If <ReturnValue>SPI_OK_SELECT</ReturnValue> is returned then you may use the global
|
||||||
pointer SPITupleTable *SPI_tuptable to access the selected tuples.
|
pointer SPITupleTable *SPI_tuptable to access the selected tuples.
|
||||||
</Para>
|
</Para>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* IDENTIFICATION
|
* IDENTIFICATION
|
||||||
* $Header: /cvsroot/pgsql/src/backend/executor/spi.c,v 1.83 2002/12/30 22:10:54 tgl Exp $
|
* $Header: /cvsroot/pgsql/src/backend/executor/spi.c,v 1.84 2003/01/21 22:06:12 tgl Exp $
|
||||||
*
|
*
|
||||||
*-------------------------------------------------------------------------
|
*-------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
@ -874,18 +874,60 @@ SPI_cursor_close(Portal portal)
|
|||||||
/* =================== private functions =================== */
|
/* =================== private functions =================== */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* spi_printtup
|
* spi_dest_setup
|
||||||
* store tuple retrieved by Executor into SPITupleTable
|
* Initialize to receive tuples from Executor into SPITupleTable
|
||||||
* of current SPI procedure
|
* of current SPI procedure
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
spi_printtup(HeapTuple tuple, TupleDesc tupdesc, DestReceiver *self)
|
spi_dest_setup(DestReceiver *self, int operation,
|
||||||
|
const char *portalName, TupleDesc typeinfo)
|
||||||
{
|
{
|
||||||
SPITupleTable *tuptable;
|
SPITupleTable *tuptable;
|
||||||
MemoryContext oldcxt;
|
MemoryContext oldcxt;
|
||||||
MemoryContext tuptabcxt;
|
MemoryContext tuptabcxt;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When called by Executor _SPI_curid expected to be equal to
|
||||||
|
* _SPI_connected
|
||||||
|
*/
|
||||||
|
if (_SPI_curid != _SPI_connected || _SPI_connected < 0)
|
||||||
|
elog(FATAL, "SPI: improper call to spi_dest_setup");
|
||||||
|
if (_SPI_current != &(_SPI_stack[_SPI_curid]))
|
||||||
|
elog(FATAL, "SPI: stack corrupted in spi_dest_setup");
|
||||||
|
|
||||||
|
if (_SPI_current->tuptable != NULL)
|
||||||
|
elog(FATAL, "SPI: improper call to spi_dest_setup");
|
||||||
|
|
||||||
|
oldcxt = _SPI_procmem(); /* switch to procedure memory context */
|
||||||
|
|
||||||
|
tuptabcxt = AllocSetContextCreate(CurrentMemoryContext,
|
||||||
|
"SPI TupTable",
|
||||||
|
ALLOCSET_DEFAULT_MINSIZE,
|
||||||
|
ALLOCSET_DEFAULT_INITSIZE,
|
||||||
|
ALLOCSET_DEFAULT_MAXSIZE);
|
||||||
|
MemoryContextSwitchTo(tuptabcxt);
|
||||||
|
|
||||||
|
_SPI_current->tuptable = tuptable = (SPITupleTable *)
|
||||||
|
palloc(sizeof(SPITupleTable));
|
||||||
|
tuptable->tuptabcxt = tuptabcxt;
|
||||||
|
tuptable->alloced = tuptable->free = 128;
|
||||||
|
tuptable->vals = (HeapTuple *) palloc(tuptable->alloced * sizeof(HeapTuple));
|
||||||
|
tuptable->tupdesc = CreateTupleDescCopy(typeinfo);
|
||||||
|
|
||||||
|
MemoryContextSwitchTo(oldcxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* spi_printtup
|
||||||
|
* store tuple retrieved by Executor into SPITupleTable
|
||||||
|
* of current SPI procedure
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
spi_printtup(HeapTuple tuple, TupleDesc tupdesc, DestReceiver *self)
|
||||||
|
{
|
||||||
|
SPITupleTable *tuptable;
|
||||||
|
MemoryContext oldcxt;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When called by Executor _SPI_curid expected to be equal to
|
* When called by Executor _SPI_curid expected to be equal to
|
||||||
* _SPI_connected
|
* _SPI_connected
|
||||||
@ -895,43 +937,24 @@ spi_printtup(HeapTuple tuple, TupleDesc tupdesc, DestReceiver *self)
|
|||||||
if (_SPI_current != &(_SPI_stack[_SPI_curid]))
|
if (_SPI_current != &(_SPI_stack[_SPI_curid]))
|
||||||
elog(FATAL, "SPI: stack corrupted in spi_printtup");
|
elog(FATAL, "SPI: stack corrupted in spi_printtup");
|
||||||
|
|
||||||
oldcxt = _SPI_procmem(); /* switch to procedure memory context */
|
|
||||||
|
|
||||||
tuptable = _SPI_current->tuptable;
|
tuptable = _SPI_current->tuptable;
|
||||||
if (tuptable == NULL)
|
if (tuptable == NULL)
|
||||||
{
|
elog(FATAL, "SPI: improper call to spi_printtup");
|
||||||
tuptabcxt = AllocSetContextCreate(CurrentMemoryContext,
|
|
||||||
"SPI TupTable",
|
|
||||||
ALLOCSET_DEFAULT_MINSIZE,
|
|
||||||
ALLOCSET_DEFAULT_INITSIZE,
|
|
||||||
ALLOCSET_DEFAULT_MAXSIZE);
|
|
||||||
MemoryContextSwitchTo(tuptabcxt);
|
|
||||||
|
|
||||||
_SPI_current->tuptable = tuptable = (SPITupleTable *)
|
oldcxt = MemoryContextSwitchTo(tuptable->tuptabcxt);
|
||||||
palloc(sizeof(SPITupleTable));
|
|
||||||
tuptable->tuptabcxt = tuptabcxt;
|
|
||||||
tuptable->alloced = tuptable->free = 128;
|
|
||||||
tuptable->vals = (HeapTuple *) palloc(tuptable->alloced * sizeof(HeapTuple));
|
|
||||||
tuptable->tupdesc = CreateTupleDescCopy(tupdesc);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MemoryContextSwitchTo(tuptable->tuptabcxt);
|
|
||||||
|
|
||||||
if (tuptable->free == 0)
|
if (tuptable->free == 0)
|
||||||
{
|
{
|
||||||
tuptable->free = 256;
|
tuptable->free = 256;
|
||||||
tuptable->alloced += tuptable->free;
|
tuptable->alloced += tuptable->free;
|
||||||
tuptable->vals = (HeapTuple *) repalloc(tuptable->vals,
|
tuptable->vals = (HeapTuple *) repalloc(tuptable->vals,
|
||||||
tuptable->alloced * sizeof(HeapTuple));
|
tuptable->alloced * sizeof(HeapTuple));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tuptable->vals[tuptable->alloced - tuptable->free] = heap_copytuple(tuple);
|
tuptable->vals[tuptable->alloced - tuptable->free] = heap_copytuple(tuple);
|
||||||
(tuptable->free)--;
|
(tuptable->free)--;
|
||||||
|
|
||||||
MemoryContextSwitchTo(oldcxt);
|
MemoryContextSwitchTo(oldcxt);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1448,19 +1471,10 @@ _SPI_checktuples(void)
|
|||||||
SPITupleTable *tuptable = _SPI_current->tuptable;
|
SPITupleTable *tuptable = _SPI_current->tuptable;
|
||||||
bool failed = false;
|
bool failed = false;
|
||||||
|
|
||||||
if (processed == 0)
|
if (tuptable == NULL) /* spi_dest_setup was not called */
|
||||||
{
|
failed = true;
|
||||||
if (tuptable != NULL)
|
else if (processed != (tuptable->alloced - tuptable->free))
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/* some tuples were processed */
|
|
||||||
if (tuptable == NULL) /* spi_printtup was not called */
|
|
||||||
failed = true;
|
|
||||||
else if (processed != (tuptable->alloced - tuptable->free))
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
* Portions Copyright (c) 1994, Regents of the University of California
|
* Portions Copyright (c) 1994, Regents of the University of California
|
||||||
*
|
*
|
||||||
* IDENTIFICATION
|
* IDENTIFICATION
|
||||||
* $Header: /cvsroot/pgsql/src/backend/tcop/dest.c,v 1.49 2002/06/20 20:29:36 momjian Exp $
|
* $Header: /cvsroot/pgsql/src/backend/tcop/dest.c,v 1.50 2003/01/21 22:06:12 tgl Exp $
|
||||||
*
|
*
|
||||||
*-------------------------------------------------------------------------
|
*-------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
@ -64,7 +64,7 @@ static DestReceiver debugtupDR = {
|
|||||||
debugtup, debugSetup, donothingCleanup
|
debugtup, debugSetup, donothingCleanup
|
||||||
};
|
};
|
||||||
static DestReceiver spi_printtupDR = {
|
static DestReceiver spi_printtupDR = {
|
||||||
spi_printtup, donothingSetup, donothingCleanup
|
spi_printtup, spi_dest_setup, donothingCleanup
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ----------------
|
/* ----------------
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
|
* Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
|
||||||
* Portions Copyright (c) 1994, Regents of the University of California
|
* Portions Copyright (c) 1994, Regents of the University of California
|
||||||
*
|
*
|
||||||
* $Id: printtup.h,v 1.22 2002/09/04 20:31:37 momjian Exp $
|
* $Id: printtup.h,v 1.23 2003/01/21 22:06:12 tgl Exp $
|
||||||
*
|
*
|
||||||
*-------------------------------------------------------------------------
|
*-------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
@ -23,7 +23,9 @@ extern void debugSetup(DestReceiver *self, int operation,
|
|||||||
extern void debugtup(HeapTuple tuple, TupleDesc typeinfo,
|
extern void debugtup(HeapTuple tuple, TupleDesc typeinfo,
|
||||||
DestReceiver *self);
|
DestReceiver *self);
|
||||||
|
|
||||||
/* XXX this one is really in executor/spi.c */
|
/* XXX these are really in executor/spi.c */
|
||||||
|
extern void spi_dest_setup(DestReceiver *self, int operation,
|
||||||
|
const char *portalName, TupleDesc typeinfo);
|
||||||
extern void spi_printtup(HeapTuple tuple, TupleDesc tupdesc,
|
extern void spi_printtup(HeapTuple tuple, TupleDesc tupdesc,
|
||||||
DestReceiver *self);
|
DestReceiver *self);
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* procedural language
|
* procedural language
|
||||||
*
|
*
|
||||||
* IDENTIFICATION
|
* IDENTIFICATION
|
||||||
* $Header: /cvsroot/pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.76 2002/12/17 15:45:01 tgl Exp $
|
* $Header: /cvsroot/pgsql/src/pl/plpgsql/src/pl_exec.c,v 1.77 2003/01/21 22:06:12 tgl Exp $
|
||||||
*
|
*
|
||||||
* This software is copyrighted by Jan Wieck - Hamburg.
|
* This software is copyrighted by Jan Wieck - Hamburg.
|
||||||
*
|
*
|
||||||
@ -1356,15 +1356,15 @@ exec_stmt_fors(PLpgSQL_execstate * estate, PLpgSQL_stmt_fors * stmt)
|
|||||||
exec_run_select(estate, stmt->query, 0, &portal);
|
exec_run_select(estate, stmt->query, 0, &portal);
|
||||||
|
|
||||||
SPI_cursor_fetch(portal, true, 10);
|
SPI_cursor_fetch(portal, true, 10);
|
||||||
n = SPI_processed;
|
|
||||||
tuptab = SPI_tuptable;
|
tuptab = SPI_tuptable;
|
||||||
|
n = SPI_processed;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the query didn't return any rows, set the target to NULL and
|
* If the query didn't return any rows, set the target to NULL and
|
||||||
* return with FOUND = false.
|
* return with FOUND = false.
|
||||||
*/
|
*/
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
exec_move_row(estate, rec, row, NULL, NULL);
|
exec_move_row(estate, rec, row, NULL, tuptab->tupdesc);
|
||||||
else
|
else
|
||||||
found = true; /* processed at least one tuple */
|
found = true; /* processed at least one tuple */
|
||||||
|
|
||||||
@ -1478,6 +1478,7 @@ exec_stmt_select(PLpgSQL_execstate * estate, PLpgSQL_stmt_select * stmt)
|
|||||||
* Run the query
|
* Run the query
|
||||||
*/
|
*/
|
||||||
exec_run_select(estate, stmt->query, 1, NULL);
|
exec_run_select(estate, stmt->query, 1, NULL);
|
||||||
|
tuptab = estate->eval_tuptable;
|
||||||
n = estate->eval_processed;
|
n = estate->eval_processed;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1486,7 +1487,7 @@ exec_stmt_select(PLpgSQL_execstate * estate, PLpgSQL_stmt_select * stmt)
|
|||||||
*/
|
*/
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
{
|
{
|
||||||
exec_move_row(estate, rec, row, NULL, NULL);
|
exec_move_row(estate, rec, row, NULL, tuptab->tupdesc);
|
||||||
exec_eval_cleanup(estate);
|
exec_eval_cleanup(estate);
|
||||||
return PLPGSQL_RC_OK;
|
return PLPGSQL_RC_OK;
|
||||||
}
|
}
|
||||||
@ -1494,7 +1495,6 @@ exec_stmt_select(PLpgSQL_execstate * estate, PLpgSQL_stmt_select * stmt)
|
|||||||
/*
|
/*
|
||||||
* Put the result into the target and set found to true
|
* Put the result into the target and set found to true
|
||||||
*/
|
*/
|
||||||
tuptab = estate->eval_tuptable;
|
|
||||||
exec_move_row(estate, rec, row, tuptab->vals[0], tuptab->tupdesc);
|
exec_move_row(estate, rec, row, tuptab->vals[0], tuptab->tupdesc);
|
||||||
exec_set_found(estate, true);
|
exec_set_found(estate, true);
|
||||||
|
|
||||||
@ -1627,6 +1627,8 @@ exec_stmt_return_next(PLpgSQL_execstate * estate,
|
|||||||
{
|
{
|
||||||
PLpgSQL_rec *rec = (PLpgSQL_rec *) (estate->datums[stmt->rec->recno]);
|
PLpgSQL_rec *rec = (PLpgSQL_rec *) (estate->datums[stmt->rec->recno]);
|
||||||
|
|
||||||
|
if (!HeapTupleIsValid(rec->tup))
|
||||||
|
elog(ERROR, "record \"%s\" is unassigned yet", rec->refname);
|
||||||
if (!compatible_tupdesc(tupdesc, rec->tupdesc))
|
if (!compatible_tupdesc(tupdesc, rec->tupdesc))
|
||||||
elog(ERROR, "Wrong record type supplied in RETURN NEXT");
|
elog(ERROR, "Wrong record type supplied in RETURN NEXT");
|
||||||
tuple = rec->tup;
|
tuple = rec->tup;
|
||||||
@ -2369,15 +2371,15 @@ exec_stmt_dynfors(PLpgSQL_execstate * estate, PLpgSQL_stmt_dynfors * stmt)
|
|||||||
* Fetch the initial 10 tuples
|
* Fetch the initial 10 tuples
|
||||||
*/
|
*/
|
||||||
SPI_cursor_fetch(portal, true, 10);
|
SPI_cursor_fetch(portal, true, 10);
|
||||||
n = SPI_processed;
|
|
||||||
tuptab = SPI_tuptable;
|
tuptab = SPI_tuptable;
|
||||||
|
n = SPI_processed;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the query didn't return any rows, set the target to NULL and
|
* If the query didn't return any rows, set the target to NULL and
|
||||||
* return with FOUND = false.
|
* return with FOUND = false.
|
||||||
*/
|
*/
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
exec_move_row(estate, rec, row, NULL, NULL);
|
exec_move_row(estate, rec, row, NULL, tuptab->tupdesc);
|
||||||
else
|
else
|
||||||
found = true;
|
found = true;
|
||||||
|
|
||||||
@ -2776,8 +2778,8 @@ exec_stmt_fetch(PLpgSQL_execstate * estate, PLpgSQL_stmt_fetch * stmt)
|
|||||||
* ----------
|
* ----------
|
||||||
*/
|
*/
|
||||||
SPI_cursor_fetch(portal, true, 1);
|
SPI_cursor_fetch(portal, true, 1);
|
||||||
n = SPI_processed;
|
|
||||||
tuptab = SPI_tuptable;
|
tuptab = SPI_tuptable;
|
||||||
|
n = SPI_processed;
|
||||||
|
|
||||||
/* ----------
|
/* ----------
|
||||||
* If the FETCH didn't return a row, set the target
|
* If the FETCH didn't return a row, set the target
|
||||||
@ -2786,7 +2788,7 @@ exec_stmt_fetch(PLpgSQL_execstate * estate, PLpgSQL_stmt_fetch * stmt)
|
|||||||
*/
|
*/
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
{
|
{
|
||||||
exec_move_row(estate, rec, row, NULL, NULL);
|
exec_move_row(estate, rec, row, NULL, tuptab->tupdesc);
|
||||||
return PLPGSQL_RC_OK;
|
return PLPGSQL_RC_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3353,8 +3355,8 @@ exec_move_row(PLpgSQL_execstate * estate,
|
|||||||
HeapTuple tup, TupleDesc tupdesc)
|
HeapTuple tup, TupleDesc tupdesc)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Record is simple - just put the tuple and its descriptor into the
|
* Record is simple - just copy the tuple and its descriptor into the
|
||||||
* record
|
* record variable
|
||||||
*/
|
*/
|
||||||
if (rec != NULL)
|
if (rec != NULL)
|
||||||
{
|
{
|
||||||
@ -3372,13 +3374,34 @@ exec_move_row(PLpgSQL_execstate * estate,
|
|||||||
if (HeapTupleIsValid(tup))
|
if (HeapTupleIsValid(tup))
|
||||||
{
|
{
|
||||||
rec->tup = heap_copytuple(tup);
|
rec->tup = heap_copytuple(tup);
|
||||||
rec->tupdesc = CreateTupleDescCopy(tupdesc);
|
|
||||||
rec->freetup = true;
|
rec->freetup = true;
|
||||||
rec->freetupdesc = true;
|
}
|
||||||
|
else if (tupdesc)
|
||||||
|
{
|
||||||
|
/* If we have a tupdesc but no data, form an all-nulls tuple */
|
||||||
|
char *nulls;
|
||||||
|
|
||||||
|
/* +1 to avoid possible palloc(0) if no attributes */
|
||||||
|
nulls = (char *) palloc(tupdesc->natts * sizeof(char) + 1);
|
||||||
|
memset(nulls, 'n', tupdesc->natts * sizeof(char));
|
||||||
|
|
||||||
|
rec->tup = heap_formtuple(tupdesc, NULL, nulls);
|
||||||
|
rec->freetup = true;
|
||||||
|
|
||||||
|
pfree(nulls);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rec->tup = NULL;
|
rec->tup = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tupdesc)
|
||||||
|
{
|
||||||
|
rec->tupdesc = CreateTupleDescCopy(tupdesc);
|
||||||
|
rec->freetupdesc = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
rec->tupdesc = NULL;
|
rec->tupdesc = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3395,6 +3418,9 @@ exec_move_row(PLpgSQL_execstate * estate,
|
|||||||
* table, or it might have fewer if the table has had columns added by
|
* table, or it might have fewer if the table has had columns added by
|
||||||
* ALTER TABLE. Ignore extra columns and assume NULL for missing
|
* ALTER TABLE. Ignore extra columns and assume NULL for missing
|
||||||
* columns, the same as heap_getattr would do.
|
* columns, the same as heap_getattr would do.
|
||||||
|
*
|
||||||
|
* If we have no tuple data at all, we'll assign NULL to all columns
|
||||||
|
* of the row variable.
|
||||||
*/
|
*/
|
||||||
if (row != NULL)
|
if (row != NULL)
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user