mirror of
https://github.com/postgres/postgres.git
synced 2025-07-26 01:22:12 +03:00
Remove the Query structure from the executor's API. This allows us to stop
storing mostly-redundant Query trees in prepared statements, portals, etc. To replace Query, a new node type called PlannedStmt is inserted by the planner at the top of a completed plan tree; this carries just the fields of Query that are still needed at runtime. The statement lists kept in portals etc. now consist of intermixed PlannedStmt and bare utility-statement nodes --- no Query. This incidentally allows us to remove some fields from Query and Plan nodes that shouldn't have been there in the first place. Still to do: simplify the execution-time range table; at the moment the range table passed to the executor still contains Query trees for subqueries. initdb forced due to change of stored rules.
This commit is contained in:
@ -8,7 +8,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.275 2007/01/25 02:17:26 momjian Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/commands/copy.c,v 1.276 2007/02/20 17:32:13 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -986,10 +986,11 @@ DoCopy(const CopyStmt *stmt)
|
||||
{
|
||||
Query *query = stmt->query;
|
||||
List *rewritten;
|
||||
Plan *plan;
|
||||
PlannedStmt *plan;
|
||||
DestReceiver *dest;
|
||||
|
||||
Assert(query);
|
||||
Assert(query->commandType == CMD_SELECT);
|
||||
Assert(!is_from);
|
||||
cstate->rel = NULL;
|
||||
|
||||
@ -999,6 +1000,7 @@ DoCopy(const CopyStmt *stmt)
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("COPY (SELECT) WITH OIDS is not supported")));
|
||||
|
||||
/* Query mustn't use INTO, either */
|
||||
if (query->into)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
@ -1016,7 +1018,6 @@ DoCopy(const CopyStmt *stmt)
|
||||
* shouldn't modify its input ... FIXME someday.
|
||||
*/
|
||||
query = copyObject(query);
|
||||
Assert(query->commandType == CMD_SELECT);
|
||||
|
||||
/*
|
||||
* Must acquire locks in case we didn't come fresh from the parser.
|
||||
@ -1051,7 +1052,7 @@ DoCopy(const CopyStmt *stmt)
|
||||
((DR_copy *) dest)->cstate = cstate;
|
||||
|
||||
/* Create a QueryDesc requesting no output */
|
||||
cstate->queryDesc = CreateQueryDesc(query, plan,
|
||||
cstate->queryDesc = CreateQueryDesc(plan,
|
||||
ActiveSnapshot, InvalidSnapshot,
|
||||
dest, NULL, false);
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
* Portions Copyright (c) 1994-5, Regents of the University of California
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.155 2007/02/19 02:23:11 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.156 2007/02/20 17:32:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -149,7 +149,7 @@ static void
|
||||
ExplainOneQuery(Query *query, ExplainStmt *stmt, ParamListInfo params,
|
||||
TupOutputState *tstate)
|
||||
{
|
||||
Plan *plan;
|
||||
PlannedStmt *plan;
|
||||
QueryDesc *queryDesc;
|
||||
bool isCursor = false;
|
||||
int cursorOptions = 0;
|
||||
@ -203,7 +203,7 @@ ExplainOneQuery(Query *query, ExplainStmt *stmt, ParamListInfo params,
|
||||
ActiveSnapshot->curcid = GetCurrentCommandId();
|
||||
|
||||
/* Create a QueryDesc requesting no output */
|
||||
queryDesc = CreateQueryDesc(query, plan,
|
||||
queryDesc = CreateQueryDesc(plan,
|
||||
ActiveSnapshot, InvalidSnapshot,
|
||||
None_Receiver, params,
|
||||
stmt->analyze);
|
||||
@ -260,14 +260,14 @@ ExplainOnePlan(QueryDesc *queryDesc, ExplainStmt *stmt,
|
||||
|
||||
es->printNodes = stmt->verbose;
|
||||
es->printAnalyze = stmt->analyze;
|
||||
es->rtable = queryDesc->parsetree->rtable;
|
||||
es->rtable = queryDesc->plannedstmt->rtable;
|
||||
|
||||
if (es->printNodes)
|
||||
{
|
||||
char *s;
|
||||
char *f;
|
||||
|
||||
s = nodeToString(queryDesc->plantree);
|
||||
s = nodeToString(queryDesc->plannedstmt->planTree);
|
||||
if (s)
|
||||
{
|
||||
if (Explain_pretty_print)
|
||||
@ -282,7 +282,8 @@ ExplainOnePlan(QueryDesc *queryDesc, ExplainStmt *stmt,
|
||||
}
|
||||
|
||||
initStringInfo(&buf);
|
||||
explain_outNode(&buf, queryDesc->plantree, queryDesc->planstate,
|
||||
explain_outNode(&buf,
|
||||
queryDesc->plannedstmt->planTree, queryDesc->planstate,
|
||||
NULL, 0, es);
|
||||
|
||||
/*
|
||||
|
@ -14,7 +14,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/commands/portalcmds.c,v 1.60 2007/02/06 22:49:24 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/commands/portalcmds.c,v 1.61 2007/02/20 17:32:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -42,7 +42,7 @@ PerformCursorOpen(DeclareCursorStmt *stmt, ParamListInfo params)
|
||||
{
|
||||
List *rewritten;
|
||||
Query *query;
|
||||
Plan *plan;
|
||||
PlannedStmt *plan;
|
||||
Portal portal;
|
||||
MemoryContext oldContext;
|
||||
|
||||
@ -98,13 +98,12 @@ PerformCursorOpen(DeclareCursorStmt *stmt, ParamListInfo params)
|
||||
plan = planner(query, true, stmt->options, params);
|
||||
|
||||
/*
|
||||
* Create a portal and copy the query and plan into its memory context.
|
||||
* Create a portal and copy the plan into its memory context.
|
||||
*/
|
||||
portal = CreatePortal(stmt->portalname, false, false);
|
||||
|
||||
oldContext = MemoryContextSwitchTo(PortalGetHeapMemory(portal));
|
||||
|
||||
query = copyObject(query);
|
||||
plan = copyObject(plan);
|
||||
|
||||
/*
|
||||
@ -115,7 +114,6 @@ PerformCursorOpen(DeclareCursorStmt *stmt, ParamListInfo params)
|
||||
NULL,
|
||||
debug_query_string ? pstrdup(debug_query_string) : NULL,
|
||||
"SELECT", /* cursor's query is always a SELECT */
|
||||
list_make1(query),
|
||||
list_make1(plan),
|
||||
PortalGetHeapMemory(portal));
|
||||
|
||||
@ -140,7 +138,7 @@ PerformCursorOpen(DeclareCursorStmt *stmt, ParamListInfo params)
|
||||
portal->cursorOptions = stmt->options;
|
||||
if (!(portal->cursorOptions & (CURSOR_OPT_SCROLL | CURSOR_OPT_NO_SCROLL)))
|
||||
{
|
||||
if (ExecSupportsBackwardScan(plan))
|
||||
if (ExecSupportsBackwardScan(plan->planTree))
|
||||
portal->cursorOptions |= CURSOR_OPT_SCROLL;
|
||||
else
|
||||
portal->cursorOptions |= CURSOR_OPT_NO_SCROLL;
|
||||
|
@ -10,7 +10,7 @@
|
||||
* Copyright (c) 2002-2007, PostgreSQL Global Development Group
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.68 2007/01/28 19:05:35 tgl Exp $
|
||||
* $PostgreSQL: pgsql/src/backend/commands/prepare.c,v 1.69 2007/02/20 17:32:14 tgl Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -114,9 +114,9 @@ PrepareQuery(PrepareStmt *stmt)
|
||||
StorePreparedStatement(stmt->name,
|
||||
debug_query_string,
|
||||
commandTag,
|
||||
query_list,
|
||||
plan_list,
|
||||
stmt->argtype_oids,
|
||||
true,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -129,8 +129,7 @@ ExecuteQuery(ExecuteStmt *stmt, ParamListInfo params,
|
||||
{
|
||||
PreparedStatement *entry;
|
||||
char *query_string;
|
||||
List *query_list,
|
||||
*plan_list;
|
||||
List *plan_list;
|
||||
MemoryContext qcontext;
|
||||
ParamListInfo paramLI = NULL;
|
||||
EState *estate = NULL;
|
||||
@ -139,12 +138,17 @@ ExecuteQuery(ExecuteStmt *stmt, ParamListInfo params,
|
||||
/* Look it up in the hash table */
|
||||
entry = FetchPreparedStatement(stmt->name, true);
|
||||
|
||||
query_string = entry->query_string;
|
||||
query_list = entry->query_list;
|
||||
plan_list = entry->plan_list;
|
||||
qcontext = entry->context;
|
||||
/*
|
||||
* Punt if not fully planned. (Currently, that only happens for the
|
||||
* protocol-level unnamed statement, which can't be accessed from SQL;
|
||||
* so there's no point in doing more than a quick check here.)
|
||||
*/
|
||||
if (!entry->fully_planned)
|
||||
elog(ERROR, "EXECUTE does not support unplanned prepared statements");
|
||||
|
||||
Assert(list_length(query_list) == list_length(plan_list));
|
||||
query_string = entry->query_string;
|
||||
plan_list = entry->stmt_list;
|
||||
qcontext = entry->context;
|
||||
|
||||
/* Evaluate parameters, if any */
|
||||
if (entry->argtype_list != NIL)
|
||||
@ -172,30 +176,26 @@ ExecuteQuery(ExecuteStmt *stmt, ParamListInfo params,
|
||||
if (stmt->into)
|
||||
{
|
||||
MemoryContext oldContext;
|
||||
Query *query;
|
||||
PlannedStmt *pstmt;
|
||||
|
||||
oldContext = MemoryContextSwitchTo(PortalGetHeapMemory(portal));
|
||||
qcontext = PortalGetHeapMemory(portal);
|
||||
oldContext = MemoryContextSwitchTo(qcontext);
|
||||
|
||||
if (query_string)
|
||||
query_string = pstrdup(query_string);
|
||||
query_list = copyObject(query_list);
|
||||
plan_list = copyObject(plan_list);
|
||||
qcontext = PortalGetHeapMemory(portal);
|
||||
|
||||
if (list_length(query_list) != 1)
|
||||
if (list_length(plan_list) != 1)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
||||
errmsg("prepared statement is not a SELECT")));
|
||||
query = (Query *) linitial(query_list);
|
||||
if (query->commandType != CMD_SELECT)
|
||||
pstmt = (PlannedStmt *) linitial(plan_list);
|
||||
if (!IsA(pstmt, PlannedStmt) ||
|
||||
pstmt->commandType != CMD_SELECT)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
||||
errmsg("prepared statement is not a SELECT")));
|
||||
query->into = copyObject(stmt->into);
|
||||
query->intoOptions = copyObject(stmt->intoOptions);
|
||||
query->intoOnCommit = stmt->into_on_commit;
|
||||
if (stmt->into_tbl_space)
|
||||
query->intoTableSpaceName = pstrdup(stmt->into_tbl_space);
|
||||
pstmt->into = copyObject(stmt->into);
|
||||
|
||||
MemoryContextSwitchTo(oldContext);
|
||||
}
|
||||
@ -204,7 +204,6 @@ ExecuteQuery(ExecuteStmt *stmt, ParamListInfo params,
|
||||
NULL,
|
||||
query_string,
|
||||
entry->commandTag,
|
||||
query_list,
|
||||
plan_list,
|
||||
qcontext);
|
||||
|
||||
@ -305,9 +304,9 @@ void
|
||||
StorePreparedStatement(const char *stmt_name,
|
||||
const char *query_string,
|
||||
const char *commandTag,
|
||||
List *query_list,
|
||||
List *plan_list,
|
||||
List *stmt_list,
|
||||
List *argtype_list,
|
||||
bool fully_planned,
|
||||
bool from_sql)
|
||||
{
|
||||
PreparedStatement *entry;
|
||||
@ -345,8 +344,7 @@ StorePreparedStatement(const char *stmt_name,
|
||||
* incomplete (ie corrupt) hashtable entry.
|
||||
*/
|
||||
qstring = query_string ? pstrdup(query_string) : NULL;
|
||||
query_list = (List *) copyObject(query_list);
|
||||
plan_list = (List *) copyObject(plan_list);
|
||||
stmt_list = (List *) copyObject(stmt_list);
|
||||
argtype_list = list_copy(argtype_list);
|
||||
|
||||
/* Now we can add entry to hash table */
|
||||
@ -363,12 +361,12 @@ StorePreparedStatement(const char *stmt_name,
|
||||
/* Fill in the hash table entry with copied data */
|
||||
entry->query_string = qstring;
|
||||
entry->commandTag = commandTag;
|
||||
entry->query_list = query_list;
|
||||
entry->plan_list = plan_list;
|
||||
entry->stmt_list = stmt_list;
|
||||
entry->argtype_list = argtype_list;
|
||||
entry->fully_planned = fully_planned;
|
||||
entry->from_sql = from_sql;
|
||||
entry->context = entrycxt;
|
||||
entry->prepare_time = GetCurrentStatementStartTimestamp();
|
||||
entry->from_sql = from_sql;
|
||||
|
||||
MemoryContextSwitchTo(oldcxt);
|
||||
}
|
||||
@ -426,21 +424,54 @@ FetchPreparedStatementParams(const char *stmt_name)
|
||||
TupleDesc
|
||||
FetchPreparedStatementResultDesc(PreparedStatement *stmt)
|
||||
{
|
||||
Node *node;
|
||||
Query *query;
|
||||
PlannedStmt *pstmt;
|
||||
|
||||
switch (ChoosePortalStrategy(stmt->query_list))
|
||||
switch (ChoosePortalStrategy(stmt->stmt_list))
|
||||
{
|
||||
case PORTAL_ONE_SELECT:
|
||||
query = (Query *) linitial(stmt->query_list);
|
||||
return ExecCleanTypeFromTL(query->targetList, false);
|
||||
node = (Node *) linitial(stmt->stmt_list);
|
||||
if (IsA(node, Query))
|
||||
{
|
||||
query = (Query *) node;
|
||||
return ExecCleanTypeFromTL(query->targetList, false);
|
||||
}
|
||||
if (IsA(node, PlannedStmt))
|
||||
{
|
||||
pstmt = (PlannedStmt *) node;
|
||||
return ExecCleanTypeFromTL(pstmt->planTree->targetlist, false);
|
||||
}
|
||||
/* other cases shouldn't happen, but return NULL */
|
||||
break;
|
||||
|
||||
case PORTAL_ONE_RETURNING:
|
||||
query = PortalListGetPrimaryQuery(stmt->query_list);
|
||||
return ExecCleanTypeFromTL(query->returningList, false);
|
||||
node = PortalListGetPrimaryStmt(stmt->stmt_list);
|
||||
if (IsA(node, Query))
|
||||
{
|
||||
query = (Query *) node;
|
||||
Assert(query->returningList);
|
||||
return ExecCleanTypeFromTL(query->returningList, false);
|
||||
}
|
||||
if (IsA(node, PlannedStmt))
|
||||
{
|
||||
pstmt = (PlannedStmt *) node;
|
||||
Assert(pstmt->returningLists);
|
||||
return ExecCleanTypeFromTL((List *) linitial(pstmt->returningLists), false);
|
||||
}
|
||||
/* other cases shouldn't happen, but return NULL */
|
||||
break;
|
||||
|
||||
case PORTAL_UTIL_SELECT:
|
||||
query = (Query *) linitial(stmt->query_list);
|
||||
return UtilityTupleDescriptor(query->utilityStmt);
|
||||
node = (Node *) linitial(stmt->stmt_list);
|
||||
if (IsA(node, Query))
|
||||
{
|
||||
query = (Query *) node;
|
||||
Assert(query->utilityStmt);
|
||||
return UtilityTupleDescriptor(query->utilityStmt);
|
||||
}
|
||||
/* else it's a bare utility statement */
|
||||
return UtilityTupleDescriptor(node);
|
||||
|
||||
case PORTAL_MULTI_QUERY:
|
||||
/* will not return tuples */
|
||||
@ -460,7 +491,7 @@ FetchPreparedStatementResultDesc(PreparedStatement *stmt)
|
||||
bool
|
||||
PreparedStatementReturnsTuples(PreparedStatement *stmt)
|
||||
{
|
||||
switch (ChoosePortalStrategy(stmt->query_list))
|
||||
switch (ChoosePortalStrategy(stmt->stmt_list))
|
||||
{
|
||||
case PORTAL_ONE_SELECT:
|
||||
case PORTAL_ONE_RETURNING:
|
||||
@ -480,52 +511,15 @@ PreparedStatementReturnsTuples(PreparedStatement *stmt)
|
||||
* targetlist.
|
||||
*
|
||||
* Note: do not modify the result.
|
||||
*
|
||||
* XXX be careful to keep this in sync with FetchPortalTargetList,
|
||||
* and with UtilityReturnsTuples.
|
||||
*/
|
||||
List *
|
||||
FetchPreparedStatementTargetList(PreparedStatement *stmt)
|
||||
{
|
||||
PortalStrategy strategy = ChoosePortalStrategy(stmt->query_list);
|
||||
|
||||
if (strategy == PORTAL_ONE_SELECT)
|
||||
return ((Query *) linitial(stmt->query_list))->targetList;
|
||||
if (strategy == PORTAL_ONE_RETURNING)
|
||||
return (PortalListGetPrimaryQuery(stmt->query_list))->returningList;
|
||||
if (strategy == PORTAL_UTIL_SELECT)
|
||||
{
|
||||
Node *utilityStmt;
|
||||
|
||||
utilityStmt = ((Query *) linitial(stmt->query_list))->utilityStmt;
|
||||
switch (nodeTag(utilityStmt))
|
||||
{
|
||||
case T_FetchStmt:
|
||||
{
|
||||
FetchStmt *substmt = (FetchStmt *) utilityStmt;
|
||||
Portal subportal;
|
||||
|
||||
Assert(!substmt->ismove);
|
||||
subportal = GetPortalByName(substmt->portalname);
|
||||
Assert(PortalIsValid(subportal));
|
||||
return FetchPortalTargetList(subportal);
|
||||
}
|
||||
|
||||
case T_ExecuteStmt:
|
||||
{
|
||||
ExecuteStmt *substmt = (ExecuteStmt *) utilityStmt;
|
||||
PreparedStatement *entry;
|
||||
|
||||
Assert(!substmt->into);
|
||||
entry = FetchPreparedStatement(substmt->name, true);
|
||||
return FetchPreparedStatementTargetList(entry);
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NIL;
|
||||
/* no point in looking if it doesn't return tuples */
|
||||
if (ChoosePortalStrategy(stmt->stmt_list) == PORTAL_MULTI_QUERY)
|
||||
return NIL;
|
||||
/* get the primary statement and find out what it returns */
|
||||
return FetchStatementTargetList(PortalListGetPrimaryStmt(stmt->stmt_list));
|
||||
}
|
||||
|
||||
/*
|
||||
@ -574,10 +568,8 @@ ExplainExecuteQuery(ExplainStmt *stmt, ParamListInfo params,
|
||||
{
|
||||
ExecuteStmt *execstmt = (ExecuteStmt *) stmt->query->utilityStmt;
|
||||
PreparedStatement *entry;
|
||||
ListCell *q,
|
||||
*p;
|
||||
List *query_list,
|
||||
*plan_list;
|
||||
List *plan_list;
|
||||
ListCell *p;
|
||||
ParamListInfo paramLI = NULL;
|
||||
EState *estate = NULL;
|
||||
|
||||
@ -587,10 +579,15 @@ ExplainExecuteQuery(ExplainStmt *stmt, ParamListInfo params,
|
||||
/* Look it up in the hash table */
|
||||
entry = FetchPreparedStatement(execstmt->name, true);
|
||||
|
||||
query_list = entry->query_list;
|
||||
plan_list = entry->plan_list;
|
||||
/*
|
||||
* Punt if not fully planned. (Currently, that only happens for the
|
||||
* protocol-level unnamed statement, which can't be accessed from SQL;
|
||||
* so there's no point in doing more than a quick check here.)
|
||||
*/
|
||||
if (!entry->fully_planned)
|
||||
elog(ERROR, "EXPLAIN EXECUTE does not support unplanned prepared statements");
|
||||
|
||||
Assert(list_length(query_list) == list_length(plan_list));
|
||||
plan_list = entry->stmt_list;
|
||||
|
||||
/* Evaluate parameters, if any */
|
||||
if (entry->argtype_list != NIL)
|
||||
@ -606,17 +603,16 @@ ExplainExecuteQuery(ExplainStmt *stmt, ParamListInfo params,
|
||||
}
|
||||
|
||||
/* Explain each query */
|
||||
forboth(q, query_list, p, plan_list)
|
||||
foreach(p, plan_list)
|
||||
{
|
||||
Query *query = (Query *) lfirst(q);
|
||||
Plan *plan = (Plan *) lfirst(p);
|
||||
PlannedStmt *pstmt = (PlannedStmt *) lfirst(p);
|
||||
bool is_last_query;
|
||||
|
||||
is_last_query = (lnext(p) == NULL);
|
||||
|
||||
if (query->commandType == CMD_UTILITY)
|
||||
if (!IsA(pstmt, PlannedStmt))
|
||||
{
|
||||
if (query->utilityStmt && IsA(query->utilityStmt, NotifyStmt))
|
||||
if (IsA(pstmt, NotifyStmt))
|
||||
do_text_output_oneline(tstate, "NOTIFY");
|
||||
else
|
||||
do_text_output_oneline(tstate, "UTILITY");
|
||||
@ -627,15 +623,15 @@ ExplainExecuteQuery(ExplainStmt *stmt, ParamListInfo params,
|
||||
|
||||
if (execstmt->into)
|
||||
{
|
||||
if (query->commandType != CMD_SELECT)
|
||||
if (pstmt->commandType != CMD_SELECT)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
|
||||
errmsg("prepared statement is not a SELECT")));
|
||||
|
||||
/* Copy the query so we can modify it */
|
||||
query = copyObject(query);
|
||||
/* Copy the stmt so we can modify it */
|
||||
pstmt = copyObject(pstmt);
|
||||
|
||||
query->into = execstmt->into;
|
||||
pstmt->into = execstmt->into;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -648,7 +644,7 @@ ExplainExecuteQuery(ExplainStmt *stmt, ParamListInfo params,
|
||||
ActiveSnapshot->curcid = GetCurrentCommandId();
|
||||
|
||||
/* Create a QueryDesc requesting no output */
|
||||
qdesc = CreateQueryDesc(query, plan,
|
||||
qdesc = CreateQueryDesc(pstmt,
|
||||
ActiveSnapshot, InvalidSnapshot,
|
||||
None_Receiver,
|
||||
paramLI, stmt->analyze);
|
||||
|
Reference in New Issue
Block a user