ddl: refactor index schema change test.
This commit is contained in:
@ -79,6 +79,19 @@ func testDropIndex(c *C, ctx context.Context, d *ddl, dbInfo *model.DBInfo, tblI
|
||||
return job
|
||||
}
|
||||
|
||||
func RemoveRecord(ctx context.Context, t table.Table, h int64, data []interface{}) error {
|
||||
if err := t.RemoveRowAllIndex(ctx, h, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove row
|
||||
if err := t.RemoveRow(ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) TestIndex(c *C) {
|
||||
tblInfo := testTableInfo(c, s.d, "t1", 3)
|
||||
ctx := testNewContext(c, s.d)
|
||||
@ -160,20 +173,303 @@ func testGetIndex(t table.Table, name string) *column.IndexedCol {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkAddIndex(c *C, state model.SchemaState, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}, isDropped bool) {
|
||||
// ctx := testNewContext(c, d)
|
||||
func (s *testIndexSuite) checkIndexKVExist(c *C, ctx context.Context, t table.Table, handle int64, indexCol *column.IndexedCol, columnValues []interface{}, isExist bool) {
|
||||
c.Assert(len(indexCol.Columns), Equals, len(columnValues))
|
||||
|
||||
txn, err := ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
exist, _, err := indexCol.X.Exist(txn, columnValues, handle)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(exist, Equals, isExist)
|
||||
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkNoneIndex(c *C, ctx context.Context, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}) {
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
|
||||
columnValues := make([]interface{}, len(indexCol.Columns))
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = row[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkDeleteOnlyIndex(c *C, ctx context.Context, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}, isDropped bool) {
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
|
||||
_, err := ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i := int64(0)
|
||||
err = t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, row)
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
columnValues := make([]interface{}, len(indexCol.Columns))
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = row[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, isDropped)
|
||||
|
||||
// Test add a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newRow := []interface{}{int64(11), int64(22), int64(33)}
|
||||
handle, err = t.AddRecord(ctx, newRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rows := [][]interface{}{row, newRow}
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, rows[i])
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(2))
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
|
||||
// Test update a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newUpdateRow := []interface{}{int64(44), int64(55), int64(66)}
|
||||
touched := map[int]bool{0: true, 1: true, 2: true}
|
||||
err = t.UpdateRecord(ctx, handle, newRow, newUpdateRow, touched)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newUpdateRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
|
||||
// Test remove a row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = RemoveRecord(ctx, t, handle, newUpdateRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkWriteOnlyIndex(c *C, ctx context.Context, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}, isDropped bool) {
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
|
||||
_, err := ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i := int64(0)
|
||||
err = t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, row)
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
columnValues := make([]interface{}, len(indexCol.Columns))
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = row[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, isDropped)
|
||||
|
||||
// Test add a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newRow := []interface{}{int64(11), int64(22), int64(33)}
|
||||
handle, err = t.AddRecord(ctx, newRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rows := [][]interface{}{row, newRow}
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, rows[i])
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(2))
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, true)
|
||||
|
||||
// Test update a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newUpdateRow := []interface{}{int64(44), int64(55), int64(66)}
|
||||
touched := map[int]bool{0: true, 1: true, 2: true}
|
||||
err = t.UpdateRecord(ctx, handle, newRow, newUpdateRow, touched)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newUpdateRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, true)
|
||||
|
||||
// Test remove a row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = RemoveRecord(ctx, t, handle, newUpdateRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkPublicIndex(c *C, ctx context.Context, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}) {
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
|
||||
_, err := ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i := int64(0)
|
||||
err = t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, row)
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
columnValues := make([]interface{}, len(indexCol.Columns))
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = row[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, true)
|
||||
|
||||
// Test add a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newRow := []interface{}{int64(11), int64(22), int64(33)}
|
||||
handle, err = t.AddRecord(ctx, newRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rows := [][]interface{}{row, newRow}
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
c.Assert(data, DeepEquals, rows[i])
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(2))
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, true)
|
||||
|
||||
// Test update a new row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
newUpdateRow := []interface{}{int64(44), int64(55), int64(66)}
|
||||
touched := map[int]bool{0: true, 1: true, 2: true}
|
||||
err = t.UpdateRecord(ctx, handle, newRow, newUpdateRow, touched)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
|
||||
for i, column := range indexCol.Columns {
|
||||
columnValues[i] = newUpdateRow[column.Offset]
|
||||
}
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, true)
|
||||
|
||||
// Test remove a row.
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = RemoveRecord(ctx, t, handle, newUpdateRow)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
i = int64(0)
|
||||
t.IterRecords(ctx, t.FirstKey(), t.Cols(), func(h int64, data []interface{}, cols []*column.Col) (bool, error) {
|
||||
i++
|
||||
return true, nil
|
||||
})
|
||||
c.Assert(i, Equals, int64(1))
|
||||
|
||||
s.checkIndexKVExist(c, ctx, t, handle, indexCol, columnValues, false)
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) checkAddOrDropIndex(c *C, state model.SchemaState, d *ddl, tblInfo *model.TableInfo, handle int64, indexCol *column.IndexedCol, row []interface{}, isDropped bool) {
|
||||
ctx := testNewContext(c, d)
|
||||
|
||||
switch state {
|
||||
case model.StateNone:
|
||||
// s.checkNoneIndex(c, ctx, d, tblInfo, handle, indexCol)
|
||||
s.checkNoneIndex(c, ctx, d, tblInfo, handle, indexCol, row)
|
||||
case model.StateDeleteOnly:
|
||||
// s.checkDeleteOnlyIndex(c, ctx, d, tblInfo, handle, indexCol, row, isDropped)
|
||||
s.checkDeleteOnlyIndex(c, ctx, d, tblInfo, handle, indexCol, row, isDropped)
|
||||
case model.StateWriteOnly:
|
||||
// s.checkWriteOnlyIndex(c, ctx, d, tblInfo, handle, indexCol, row, isDropped)
|
||||
s.checkWriteOnlyIndex(c, ctx, d, tblInfo, handle, indexCol, row, isDropped)
|
||||
case model.StateReorganization:
|
||||
// do nothing
|
||||
case model.StatePublic:
|
||||
// s.checkPublicIndex(c, ctx, d, tblInfo, handle, indexCol, row)
|
||||
s.checkPublicIndex(c, ctx, d, tblInfo, handle, indexCol, row)
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,7 +506,7 @@ func (s *testIndexSuite) TestAddIndex(c *C) {
|
||||
return
|
||||
}
|
||||
|
||||
s.checkAddIndex(c, indexCol.State, d, tblInfo, handle, indexCol, row, false)
|
||||
s.checkAddOrDropIndex(c, indexCol.State, d, tblInfo, handle, indexCol, row, false)
|
||||
|
||||
if indexCol.State == model.StatePublic {
|
||||
checkOK = true
|
||||
@ -241,149 +537,72 @@ func (s *testIndexSuite) TestAddIndex(c *C) {
|
||||
s.d.start()
|
||||
}
|
||||
|
||||
func (s *testIndexSuite) TestIndexState(c *C) {
|
||||
func (s *testIndexSuite) TestDropIndex(c *C) {
|
||||
d := newDDL(s.store, nil, nil, 100*time.Millisecond)
|
||||
defer d.close()
|
||||
|
||||
tblInfo := testTableInfo(c, d, "t", 3)
|
||||
ctx := testNewContext(c, d)
|
||||
defer ctx.FinishTxn(true)
|
||||
|
||||
txn, err := ctx.GetTxn(true)
|
||||
_, err := ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
testCreateTable(c, ctx, d, s.dbInfo, tblInfo)
|
||||
defer testDropTable(c, ctx, d, s.dbInfo, tblInfo)
|
||||
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
|
||||
var h int64
|
||||
h, err = t.AddRecord(ctx, []interface{}{1, 1, 1})
|
||||
row := []interface{}{int64(1), int64(2), int64(3)}
|
||||
handle, err := t.AddRecord(ctx, row)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
ticker := time.NewTicker(d.lease)
|
||||
done := make(chan *model.Job, 1)
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
ctx.FinishTxn(false)
|
||||
go func() {
|
||||
done <- testCreateIndex(c, ctx, d, s.dbInfo, tblInfo, true, "c1_uni", "c1")
|
||||
}()
|
||||
job := testCreateIndex(c, ctx, s.d, s.dbInfo, tblInfo, true, "c1_uni", "c1")
|
||||
testCheckJobDone(c, d, job, true)
|
||||
|
||||
var exist bool
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case job := <-done:
|
||||
testCheckJobDone(c, d, job, true)
|
||||
break LOOP
|
||||
case <-ticker.C:
|
||||
d.close()
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
t = testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
index := testGetIndex(t, "c1")
|
||||
if index == nil {
|
||||
d.start()
|
||||
continue
|
||||
}
|
||||
checkOK := false
|
||||
oldIndexCol := &column.IndexedCol{}
|
||||
|
||||
err = t.RemoveRowAllIndex(ctx, h, []interface{}{1})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = t.RemoveRow(ctx, h)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
txn, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
exist, _, err = index.X.Exist(txn, []interface{}{1}, h)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(exist, IsFalse)
|
||||
|
||||
h, err = t.AddRecord(ctx, []interface{}{1, 1, 1})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
txn, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
exist, _, err = index.X.Exist(txn, []interface{}{1}, h)
|
||||
c.Assert(err, IsNil)
|
||||
switch index.State {
|
||||
case model.StateDeleteOnly:
|
||||
c.Assert(exist, IsFalse)
|
||||
case model.StateNone:
|
||||
c.Fatalf("can be none state")
|
||||
default:
|
||||
c.Assert(exist, IsTrue)
|
||||
}
|
||||
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
d.start()
|
||||
tc := &testDDLCallback{}
|
||||
tc.onJobUpdated = func(job *model.Job) {
|
||||
if checkOK {
|
||||
return
|
||||
}
|
||||
|
||||
t := testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
indexCol := testGetIndex(t, "c1")
|
||||
if indexCol == nil {
|
||||
s.checkAddOrDropIndex(c, model.StateNone, d, tblInfo, handle, oldIndexCol, row, true)
|
||||
checkOK = true
|
||||
return
|
||||
}
|
||||
|
||||
s.checkAddOrDropIndex(c, indexCol.State, d, tblInfo, handle, indexCol, row, true)
|
||||
oldIndexCol = indexCol
|
||||
}
|
||||
|
||||
t = testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
c.Assert(t.FindIndexByColName("c1"), NotNil)
|
||||
d.hook = tc
|
||||
|
||||
ctx.FinishTxn(false)
|
||||
go func() {
|
||||
done <- testDropIndex(c, ctx, d, s.dbInfo, tblInfo, "c1_uni")
|
||||
}()
|
||||
// Use local ddl for callback test.
|
||||
s.d.close()
|
||||
|
||||
LOOP1:
|
||||
for {
|
||||
select {
|
||||
case job := <-done:
|
||||
testCheckJobDone(c, d, job, false)
|
||||
break LOOP1
|
||||
case <-ticker.C:
|
||||
d.close()
|
||||
d.close()
|
||||
d.start()
|
||||
|
||||
t = testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
index := testGetIndex(t, "c1")
|
||||
if index == nil {
|
||||
d.start()
|
||||
continue
|
||||
}
|
||||
job = testDropIndex(c, ctx, d, s.dbInfo, tblInfo, "c1_uni")
|
||||
testCheckJobDone(c, d, job, false)
|
||||
|
||||
err = t.RemoveRowAllIndex(ctx, h, []interface{}{1})
|
||||
c.Assert(err, IsNil)
|
||||
_, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = t.RemoveRow(ctx, h)
|
||||
c.Assert(err, IsNil)
|
||||
job = testDropTable(c, ctx, d, s.dbInfo, tblInfo)
|
||||
testCheckJobDone(c, d, job, false)
|
||||
|
||||
txn, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
exist, _, err = index.X.Exist(txn, []interface{}{1}, h)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(exist, IsFalse)
|
||||
|
||||
h, err = t.AddRecord(ctx, []interface{}{1, 1, 1})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
txn, err = ctx.GetTxn(true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
exist, _, err = index.X.Exist(txn, []interface{}{1}, h)
|
||||
c.Assert(err, IsNil)
|
||||
switch index.State {
|
||||
case model.StateDeleteOnly:
|
||||
c.Assert(exist, IsFalse)
|
||||
case model.StateNone:
|
||||
c.Fatalf("can be none state")
|
||||
default:
|
||||
c.Assert(exist, IsTrue)
|
||||
}
|
||||
|
||||
err = ctx.FinishTxn(false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
d.start()
|
||||
}
|
||||
}
|
||||
|
||||
t = testGetTable(c, d, s.dbInfo.ID, tblInfo.ID)
|
||||
c.Assert(t.FindIndexByColName("c1"), IsNil)
|
||||
d.close()
|
||||
s.d.start()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user