mirror of
https://github.com/postgres/postgres.git
synced 2025-07-12 21:01:52 +03:00
Per-column collation support
This adds collation support for columns and domains, a COLLATE clause to override it per expression, and B-tree index support. Peter Eisentraut reviewed by Pavel Stehule, Itagaki Takahiro, Robert Haas, Noah Misch
This commit is contained in:
@ -1795,6 +1795,7 @@ cost_mergejoin(MergePath *path, PlannerInfo *root, SpecialJoinInfo *sjinfo)
|
||||
ipathkey = (PathKey *) linitial(ipathkeys);
|
||||
/* debugging check */
|
||||
if (opathkey->pk_opfamily != ipathkey->pk_opfamily ||
|
||||
opathkey->pk_collation != ipathkey->pk_collation ||
|
||||
opathkey->pk_strategy != ipathkey->pk_strategy ||
|
||||
opathkey->pk_nulls_first != ipathkey->pk_nulls_first)
|
||||
elog(ERROR, "left and right pathkeys do not match in mergejoin");
|
||||
@ -2045,6 +2046,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
|
||||
{
|
||||
cache = (MergeScanSelCache *) lfirst(lc);
|
||||
if (cache->opfamily == pathkey->pk_opfamily &&
|
||||
cache->collation == pathkey->pk_collation &&
|
||||
cache->strategy == pathkey->pk_strategy &&
|
||||
cache->nulls_first == pathkey->pk_nulls_first)
|
||||
return cache;
|
||||
@ -2054,6 +2056,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
|
||||
mergejoinscansel(root,
|
||||
(Node *) rinfo->clause,
|
||||
pathkey->pk_opfamily,
|
||||
pathkey->pk_collation,
|
||||
pathkey->pk_strategy,
|
||||
pathkey->pk_nulls_first,
|
||||
&leftstartsel,
|
||||
@ -2066,6 +2069,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey)
|
||||
|
||||
cache = (MergeScanSelCache *) palloc(sizeof(MergeScanSelCache));
|
||||
cache->opfamily = pathkey->pk_opfamily;
|
||||
cache->collation = pathkey->pk_collation;
|
||||
cache->strategy = pathkey->pk_strategy;
|
||||
cache->nulls_first = pathkey->pk_nulls_first;
|
||||
cache->leftstartsel = leftstartsel;
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "catalog/pg_opfamily.h"
|
||||
#include "catalog/pg_type.h"
|
||||
#include "nodes/makefuncs.h"
|
||||
#include "nodes/nodeFuncs.h"
|
||||
#include "optimizer/clauses.h"
|
||||
#include "optimizer/cost.h"
|
||||
#include "optimizer/pathnode.h"
|
||||
@ -99,15 +100,15 @@ static List *find_clauses_for_join(PlannerInfo *root, RelOptInfo *rel,
|
||||
Relids outer_relids, bool isouterjoin);
|
||||
static bool match_boolean_index_clause(Node *clause, int indexcol,
|
||||
IndexOptInfo *index);
|
||||
static bool match_special_index_operator(Expr *clause, Oid opfamily,
|
||||
static bool match_special_index_operator(Expr *clause, Oid idxcolcollation, Oid opfamily,
|
||||
bool indexkey_on_left);
|
||||
static Expr *expand_boolean_index_clause(Node *clause, int indexcol,
|
||||
IndexOptInfo *index);
|
||||
static List *expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily);
|
||||
static List *expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily, Oid collation);
|
||||
static RestrictInfo *expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
IndexOptInfo *index,
|
||||
int indexcol);
|
||||
static List *prefix_quals(Node *leftop, Oid opfamily,
|
||||
static List *prefix_quals(Node *leftop, Oid opfamily, Oid collation,
|
||||
Const *prefix, Pattern_Prefix_Status pstatus);
|
||||
static List *network_prefix_quals(Node *leftop, Oid expr_op, Oid opfamily,
|
||||
Datum rightop);
|
||||
@ -1142,7 +1143,9 @@ group_clauses_by_indexkey(IndexOptInfo *index,
|
||||
* and
|
||||
* (2) must contain an operator which is in the same family as the index
|
||||
* operator for this column, or is a "special" operator as recognized
|
||||
* by match_special_index_operator().
|
||||
* by match_special_index_operator();
|
||||
* and
|
||||
* (3) must match the collation of the index.
|
||||
*
|
||||
* Our definition of "const" is pretty liberal: we allow Vars belonging
|
||||
* to the caller-specified outer_relids relations (which had better not
|
||||
@ -1198,6 +1201,7 @@ match_clause_to_indexcol(IndexOptInfo *index,
|
||||
SaOpControl saop_control)
|
||||
{
|
||||
Expr *clause = rinfo->clause;
|
||||
Oid collation = index->indexcollations[indexcol];
|
||||
Oid opfamily = index->opfamily[indexcol];
|
||||
Node *leftop,
|
||||
*rightop;
|
||||
@ -1280,7 +1284,8 @@ match_clause_to_indexcol(IndexOptInfo *index,
|
||||
bms_is_subset(right_relids, outer_relids) &&
|
||||
!contain_volatile_functions(rightop))
|
||||
{
|
||||
if (is_indexable_operator(expr_op, opfamily, true))
|
||||
if (is_indexable_operator(expr_op, opfamily, true) &&
|
||||
(!collation || collation == exprCollation((Node *) clause)))
|
||||
return true;
|
||||
|
||||
/*
|
||||
@ -1288,7 +1293,7 @@ match_clause_to_indexcol(IndexOptInfo *index,
|
||||
* is a "special" indexable operator.
|
||||
*/
|
||||
if (plain_op &&
|
||||
match_special_index_operator(clause, opfamily, true))
|
||||
match_special_index_operator(clause, collation, opfamily, true))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@ -1298,14 +1303,15 @@ match_clause_to_indexcol(IndexOptInfo *index,
|
||||
bms_is_subset(left_relids, outer_relids) &&
|
||||
!contain_volatile_functions(leftop))
|
||||
{
|
||||
if (is_indexable_operator(expr_op, opfamily, false))
|
||||
if (is_indexable_operator(expr_op, opfamily, false) &&
|
||||
(!collation || collation == exprCollation((Node *) clause)))
|
||||
return true;
|
||||
|
||||
/*
|
||||
* If we didn't find a member of the index's opfamily, see whether it
|
||||
* is a "special" indexable operator.
|
||||
*/
|
||||
if (match_special_index_operator(clause, opfamily, false))
|
||||
if (match_special_index_operator(clause, collation, opfamily, false))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@ -1391,6 +1397,9 @@ match_rowcompare_to_indexcol(IndexOptInfo *index,
|
||||
else
|
||||
return false;
|
||||
|
||||
if (index->indexcollations[indexcol] != linitial_oid(clause->collids))
|
||||
return false;
|
||||
|
||||
/* We're good if the operator is the right type of opfamily member */
|
||||
switch (get_op_opfamily_strategy(expr_op, opfamily))
|
||||
{
|
||||
@ -2380,7 +2389,7 @@ match_boolean_index_clause(Node *clause,
|
||||
* Return 'true' if we can do something with it anyway.
|
||||
*/
|
||||
static bool
|
||||
match_special_index_operator(Expr *clause, Oid opfamily,
|
||||
match_special_index_operator(Expr *clause, Oid idxcolcollation, Oid opfamily,
|
||||
bool indexkey_on_left)
|
||||
{
|
||||
bool isIndexable = false;
|
||||
@ -2495,7 +2504,7 @@ match_special_index_operator(Expr *clause, Oid opfamily,
|
||||
isIndexable =
|
||||
(opfamily == TEXT_PATTERN_BTREE_FAM_OID) ||
|
||||
(opfamily == TEXT_BTREE_FAM_OID &&
|
||||
(pstatus == Pattern_Prefix_Exact || lc_collate_is_c()));
|
||||
(pstatus == Pattern_Prefix_Exact || lc_collate_is_c(idxcolcollation)));
|
||||
break;
|
||||
|
||||
case OID_BPCHAR_LIKE_OP:
|
||||
@ -2505,7 +2514,7 @@ match_special_index_operator(Expr *clause, Oid opfamily,
|
||||
isIndexable =
|
||||
(opfamily == BPCHAR_PATTERN_BTREE_FAM_OID) ||
|
||||
(opfamily == BPCHAR_BTREE_FAM_OID &&
|
||||
(pstatus == Pattern_Prefix_Exact || lc_collate_is_c()));
|
||||
(pstatus == Pattern_Prefix_Exact || lc_collate_is_c(idxcolcollation)));
|
||||
break;
|
||||
|
||||
case OID_NAME_LIKE_OP:
|
||||
@ -2526,6 +2535,25 @@ match_special_index_operator(Expr *clause, Oid opfamily,
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isIndexable)
|
||||
return false;
|
||||
|
||||
/*
|
||||
* For case-insensitive matching, we also need to check that the
|
||||
* collations match.
|
||||
*/
|
||||
switch (expr_op)
|
||||
{
|
||||
case OID_TEXT_ICLIKE_OP:
|
||||
case OID_TEXT_ICREGEXEQ_OP:
|
||||
case OID_BPCHAR_ICLIKE_OP:
|
||||
case OID_BPCHAR_ICREGEXEQ_OP:
|
||||
case OID_NAME_ICLIKE_OP:
|
||||
case OID_NAME_ICREGEXEQ_OP:
|
||||
isIndexable = (idxcolcollation == exprCollation((Node *) clause));
|
||||
break;
|
||||
}
|
||||
|
||||
return isIndexable;
|
||||
}
|
||||
|
||||
@ -2561,6 +2589,7 @@ expand_indexqual_conditions(IndexOptInfo *index, List *clausegroups)
|
||||
{
|
||||
List *clausegroup = (List *) lfirst(lc);
|
||||
Oid curFamily = index->opfamily[indexcol];
|
||||
Oid curCollation = index->indexcollations[indexcol];
|
||||
ListCell *lc2;
|
||||
|
||||
foreach(lc2, clausegroup)
|
||||
@ -2592,7 +2621,8 @@ expand_indexqual_conditions(IndexOptInfo *index, List *clausegroups)
|
||||
{
|
||||
resultquals = list_concat(resultquals,
|
||||
expand_indexqual_opclause(rinfo,
|
||||
curFamily));
|
||||
curFamily,
|
||||
curCollation));
|
||||
}
|
||||
else if (IsA(clause, ScalarArrayOpExpr))
|
||||
{
|
||||
@ -2693,7 +2723,7 @@ expand_boolean_index_clause(Node *clause,
|
||||
* expand special cases that were accepted by match_special_index_operator().
|
||||
*/
|
||||
static List *
|
||||
expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily)
|
||||
expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily, Oid collation)
|
||||
{
|
||||
Expr *clause = rinfo->clause;
|
||||
|
||||
@ -2724,7 +2754,7 @@ expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily)
|
||||
{
|
||||
pstatus = pattern_fixed_prefix(patt, Pattern_Type_Like,
|
||||
&prefix, &rest);
|
||||
return prefix_quals(leftop, opfamily, prefix, pstatus);
|
||||
return prefix_quals(leftop, opfamily, collation, prefix, pstatus);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -2736,7 +2766,7 @@ expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily)
|
||||
/* the right-hand const is type text for all of these */
|
||||
pstatus = pattern_fixed_prefix(patt, Pattern_Type_Like_IC,
|
||||
&prefix, &rest);
|
||||
return prefix_quals(leftop, opfamily, prefix, pstatus);
|
||||
return prefix_quals(leftop, opfamily, collation, prefix, pstatus);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -2748,7 +2778,7 @@ expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily)
|
||||
/* the right-hand const is type text for all of these */
|
||||
pstatus = pattern_fixed_prefix(patt, Pattern_Type_Regex,
|
||||
&prefix, &rest);
|
||||
return prefix_quals(leftop, opfamily, prefix, pstatus);
|
||||
return prefix_quals(leftop, opfamily, collation, prefix, pstatus);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -2760,7 +2790,7 @@ expand_indexqual_opclause(RestrictInfo *rinfo, Oid opfamily)
|
||||
/* the right-hand const is type text for all of these */
|
||||
pstatus = pattern_fixed_prefix(patt, Pattern_Type_Regex_IC,
|
||||
&prefix, &rest);
|
||||
return prefix_quals(leftop, opfamily, prefix, pstatus);
|
||||
return prefix_quals(leftop, opfamily, collation, prefix, pstatus);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -2814,6 +2844,7 @@ expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
ListCell *largs_cell;
|
||||
ListCell *rargs_cell;
|
||||
ListCell *opnos_cell;
|
||||
ListCell *collids_cell;
|
||||
|
||||
/* We have to figure out (again) how the first col matches */
|
||||
var_on_left = match_index_to_operand((Node *) linitial(clause->largs),
|
||||
@ -2845,6 +2876,7 @@ expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
largs_cell = lnext(list_head(clause->largs));
|
||||
rargs_cell = lnext(list_head(clause->rargs));
|
||||
opnos_cell = lnext(list_head(clause->opnos));
|
||||
collids_cell = lnext(list_head(clause->collids));
|
||||
|
||||
while (largs_cell != NULL)
|
||||
{
|
||||
@ -2891,6 +2923,10 @@ expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
!= op_strategy)
|
||||
break;
|
||||
|
||||
/* Does collation match? */
|
||||
if (lfirst_oid(collids_cell) != index->indexcollations[i])
|
||||
break;
|
||||
|
||||
/* Add opfamily and datatypes to lists */
|
||||
get_op_opfamily_properties(expr_op, index->opfamily[i], false,
|
||||
&op_strategy,
|
||||
@ -2974,6 +3010,8 @@ expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
rc->opnos = new_ops;
|
||||
rc->opfamilies = list_truncate(list_copy(clause->opfamilies),
|
||||
matching_cols);
|
||||
rc->collids = list_truncate(list_copy(clause->collids),
|
||||
matching_cols);
|
||||
rc->largs = list_truncate((List *) copyObject(clause->largs),
|
||||
matching_cols);
|
||||
rc->rargs = list_truncate((List *) copyObject(clause->rargs),
|
||||
@ -2998,7 +3036,7 @@ expand_indexqual_rowcompare(RestrictInfo *rinfo,
|
||||
* operators and operand datatypes.
|
||||
*/
|
||||
static List *
|
||||
prefix_quals(Node *leftop, Oid opfamily,
|
||||
prefix_quals(Node *leftop, Oid opfamily, Oid collation,
|
||||
Const *prefix_const, Pattern_Prefix_Status pstatus)
|
||||
{
|
||||
List *result;
|
||||
@ -3100,6 +3138,7 @@ prefix_quals(Node *leftop, Oid opfamily,
|
||||
if (oproid == InvalidOid)
|
||||
elog(ERROR, "no < operator for opfamily %u", opfamily);
|
||||
fmgr_info(get_opcode(oproid), <proc);
|
||||
fmgr_info_collation(collation, <proc);
|
||||
greaterstr = make_greater_string(prefix_const, <proc);
|
||||
if (greaterstr)
|
||||
{
|
||||
|
@ -18,6 +18,7 @@
|
||||
#include "postgres.h"
|
||||
|
||||
#include "access/skey.h"
|
||||
#include "catalog/pg_collation.h"
|
||||
#include "catalog/pg_type.h"
|
||||
#include "nodes/makefuncs.h"
|
||||
#include "nodes/nodeFuncs.h"
|
||||
@ -30,10 +31,10 @@
|
||||
#include "utils/lsyscache.h"
|
||||
|
||||
|
||||
static PathKey *makePathKey(EquivalenceClass *eclass, Oid opfamily,
|
||||
static PathKey *makePathKey(EquivalenceClass *eclass, Oid opfamily, Oid collation,
|
||||
int strategy, bool nulls_first);
|
||||
static PathKey *make_canonical_pathkey(PlannerInfo *root,
|
||||
EquivalenceClass *eclass, Oid opfamily,
|
||||
EquivalenceClass *eclass, Oid opfamily, Oid collation,
|
||||
int strategy, bool nulls_first);
|
||||
static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
|
||||
static Var *find_indexkey_var(PlannerInfo *root, RelOptInfo *rel,
|
||||
@ -53,13 +54,14 @@ static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
|
||||
* convenience routine to build the specified node.
|
||||
*/
|
||||
static PathKey *
|
||||
makePathKey(EquivalenceClass *eclass, Oid opfamily,
|
||||
makePathKey(EquivalenceClass *eclass, Oid opfamily, Oid collation,
|
||||
int strategy, bool nulls_first)
|
||||
{
|
||||
PathKey *pk = makeNode(PathKey);
|
||||
|
||||
pk->pk_eclass = eclass;
|
||||
pk->pk_opfamily = opfamily;
|
||||
pk->pk_collation = collation;
|
||||
pk->pk_strategy = strategy;
|
||||
pk->pk_nulls_first = nulls_first;
|
||||
|
||||
@ -77,7 +79,7 @@ makePathKey(EquivalenceClass *eclass, Oid opfamily,
|
||||
*/
|
||||
static PathKey *
|
||||
make_canonical_pathkey(PlannerInfo *root,
|
||||
EquivalenceClass *eclass, Oid opfamily,
|
||||
EquivalenceClass *eclass, Oid opfamily, Oid collation,
|
||||
int strategy, bool nulls_first)
|
||||
{
|
||||
PathKey *pk;
|
||||
@ -93,6 +95,7 @@ make_canonical_pathkey(PlannerInfo *root,
|
||||
pk = (PathKey *) lfirst(lc);
|
||||
if (eclass == pk->pk_eclass &&
|
||||
opfamily == pk->pk_opfamily &&
|
||||
collation == pk->pk_collation &&
|
||||
strategy == pk->pk_strategy &&
|
||||
nulls_first == pk->pk_nulls_first)
|
||||
return pk;
|
||||
@ -104,7 +107,7 @@ make_canonical_pathkey(PlannerInfo *root,
|
||||
*/
|
||||
oldcontext = MemoryContextSwitchTo(root->planner_cxt);
|
||||
|
||||
pk = makePathKey(eclass, opfamily, strategy, nulls_first);
|
||||
pk = makePathKey(eclass, opfamily, collation, strategy, nulls_first);
|
||||
root->canon_pathkeys = lappend(root->canon_pathkeys, pk);
|
||||
|
||||
MemoryContextSwitchTo(oldcontext);
|
||||
@ -206,6 +209,7 @@ canonicalize_pathkeys(PlannerInfo *root, List *pathkeys)
|
||||
cpathkey = make_canonical_pathkey(root,
|
||||
eclass,
|
||||
pathkey->pk_opfamily,
|
||||
pathkey->pk_collation,
|
||||
pathkey->pk_strategy,
|
||||
pathkey->pk_nulls_first);
|
||||
|
||||
@ -247,6 +251,7 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
|
||||
Oid equality_op;
|
||||
List *opfamilies;
|
||||
EquivalenceClass *eclass;
|
||||
Oid collation;
|
||||
|
||||
strategy = reverse_sort ? BTGreaterStrategyNumber : BTLessStrategyNumber;
|
||||
|
||||
@ -301,12 +306,14 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
|
||||
if (!eclass)
|
||||
return NULL;
|
||||
|
||||
collation = exprCollation((Node *) expr);
|
||||
|
||||
/* And finally we can find or create a PathKey node */
|
||||
if (canonicalize)
|
||||
return make_canonical_pathkey(root, eclass, opfamily,
|
||||
return make_canonical_pathkey(root, eclass, opfamily, collation,
|
||||
strategy, nulls_first);
|
||||
else
|
||||
return makePathKey(eclass, opfamily, strategy, nulls_first);
|
||||
return makePathKey(eclass, opfamily, collation, strategy, nulls_first);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -605,7 +612,8 @@ find_indexkey_var(PlannerInfo *root, RelOptInfo *rel, AttrNumber varattno)
|
||||
ListCell *temp;
|
||||
Index relid;
|
||||
Oid reloid,
|
||||
vartypeid;
|
||||
vartypeid,
|
||||
varcollid;
|
||||
int32 type_mod;
|
||||
|
||||
foreach(temp, rel->reltargetlist)
|
||||
@ -620,8 +628,9 @@ find_indexkey_var(PlannerInfo *root, RelOptInfo *rel, AttrNumber varattno)
|
||||
relid = rel->relid;
|
||||
reloid = getrelid(relid, root->parse->rtable);
|
||||
get_atttypetypmod(reloid, varattno, &vartypeid, &type_mod);
|
||||
varcollid = get_attcollation(reloid, varattno);
|
||||
|
||||
return makeVar(relid, varattno, vartypeid, type_mod, 0);
|
||||
return makeVar(relid, varattno, vartypeid, type_mod, varcollid, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -703,6 +712,7 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
|
||||
make_canonical_pathkey(root,
|
||||
outer_ec,
|
||||
sub_pathkey->pk_opfamily,
|
||||
sub_pathkey->pk_collation,
|
||||
sub_pathkey->pk_strategy,
|
||||
sub_pathkey->pk_nulls_first);
|
||||
}
|
||||
@ -805,6 +815,7 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel,
|
||||
outer_pk = make_canonical_pathkey(root,
|
||||
outer_ec,
|
||||
sub_pathkey->pk_opfamily,
|
||||
sub_pathkey->pk_collation,
|
||||
sub_pathkey->pk_strategy,
|
||||
sub_pathkey->pk_nulls_first);
|
||||
/* score = # of equivalence peers */
|
||||
@ -1326,6 +1337,7 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
|
||||
pathkey = make_canonical_pathkey(root,
|
||||
ec,
|
||||
linitial_oid(ec->ec_opfamilies),
|
||||
DEFAULT_COLLATION_OID,
|
||||
BTLessStrategyNumber,
|
||||
false);
|
||||
/* can't be redundant because no duplicate ECs */
|
||||
@ -1419,6 +1431,7 @@ make_inner_pathkeys_for_merge(PlannerInfo *root,
|
||||
pathkey = make_canonical_pathkey(root,
|
||||
ieclass,
|
||||
opathkey->pk_opfamily,
|
||||
opathkey->pk_collation,
|
||||
opathkey->pk_strategy,
|
||||
opathkey->pk_nulls_first);
|
||||
|
||||
@ -1539,6 +1552,7 @@ right_merge_direction(PlannerInfo *root, PathKey *pathkey)
|
||||
PathKey *query_pathkey = (PathKey *) lfirst(l);
|
||||
|
||||
if (pathkey->pk_eclass == query_pathkey->pk_eclass &&
|
||||
pathkey->pk_collation == query_pathkey->pk_collation &&
|
||||
pathkey->pk_opfamily == query_pathkey->pk_opfamily)
|
||||
{
|
||||
/*
|
||||
|
Reference in New Issue
Block a user