mirror of
https://github.com/CarmJos/EasySQL.git
synced 2024-09-19 13:25:47 +00:00
feat(migrate): 添加对表迁移的操作 (#69)
* DELETE语句缺少AND关键词连接 * feat(migrate): 添加对表迁移的操作 * Update SQLMigrate.java Co-authored-by: sonatype-lift[bot] <37194012+sonatype-lift[bot]@users.noreply.github.com> * feat(migrate): 添加对表迁移的操作 --------- Co-authored-by: sonatype-lift[bot] <37194012+sonatype-lift[bot]@users.noreply.github.com>
This commit is contained in:
parent
179686b4cf
commit
c677731f6a
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
**/target/
|
**/target/
|
||||||
**.iml
|
**.iml
|
||||||
|
demo/logs
|
@ -9,6 +9,7 @@ import cc.carm.lib.easysql.api.function.SQLBiFunction;
|
|||||||
import cc.carm.lib.easysql.api.function.SQLDebugHandler;
|
import cc.carm.lib.easysql.api.function.SQLDebugHandler;
|
||||||
import cc.carm.lib.easysql.api.function.SQLExceptionHandler;
|
import cc.carm.lib.easysql.api.function.SQLExceptionHandler;
|
||||||
import cc.carm.lib.easysql.api.function.SQLFunction;
|
import cc.carm.lib.easysql.api.function.SQLFunction;
|
||||||
|
import cc.carm.lib.easysql.api.migrate.SQLMigrate;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -265,6 +266,13 @@ public interface SQLManager {
|
|||||||
*/
|
*/
|
||||||
QueryBuilder createQuery();
|
QueryBuilder createQuery();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建一个迁移操作。
|
||||||
|
*
|
||||||
|
* @return {@link SQLMigrate}
|
||||||
|
*/
|
||||||
|
SQLMigrate createMigrate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建一条插入操作。
|
* 创建一条插入操作。
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
package cc.carm.lib.easysql.api.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2022/11/28<br>
|
||||||
|
* EasySQL<br>
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
*/
|
||||||
|
public class MigrateResult {
|
||||||
|
/**
|
||||||
|
* 旧表不存在
|
||||||
|
*/
|
||||||
|
public static final MigrateResult OLD_TABLE_NOT_EXIST = new MigrateResult(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新表已存在
|
||||||
|
*/
|
||||||
|
public static final MigrateResult NEW_TABLE_EXIST = new MigrateResult(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新表字段为空
|
||||||
|
*/
|
||||||
|
public static final MigrateResult NEW_COLUMN_EMPTY = new MigrateResult(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧表字段为在新表这设置对应的字段名
|
||||||
|
*/
|
||||||
|
public static final MigrateResult COLUMN_NOT_SET = new MigrateResult(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移成功
|
||||||
|
*/
|
||||||
|
public static final MigrateResult SUCCESS = new MigrateResult(true);
|
||||||
|
|
||||||
|
private final boolean success;
|
||||||
|
private Throwable error;
|
||||||
|
|
||||||
|
MigrateResult(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MigrateResult(boolean success, Throwable error) {
|
||||||
|
this.success = success;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MigrateResult from(MigrateResult migrateResult, Throwable error) {
|
||||||
|
return new MigrateResult(migrateResult.success, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Throwable error() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package cc.carm.lib.easysql.api.migrate;
|
||||||
|
|
||||||
|
import cc.carm.lib.easysql.api.enums.IndexType;
|
||||||
|
import cc.carm.lib.easysql.api.enums.MigrateResult;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将现有的表结构与数据迁移之新的表中
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
* @since 0.4.7
|
||||||
|
*/
|
||||||
|
public interface SQLMigrate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为迁移工作定义前后的表名信息
|
||||||
|
*
|
||||||
|
* @param oldTableName 旧表名
|
||||||
|
* @param newTableName 新表名
|
||||||
|
* @param newTableSettings 新表的设置
|
||||||
|
* @return {@link SQLMigrate}
|
||||||
|
*/
|
||||||
|
@Contract("null,_,_ -> fail;!null,_,_ -> this")
|
||||||
|
SQLMigrate tableName(String oldTableName, String newTableName, String newTableSettings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为迁移工作定义前后的列名信息
|
||||||
|
*
|
||||||
|
* @param oldColumnName 旧列名
|
||||||
|
* @param newColumnName 新列名
|
||||||
|
* @param newColumnSettings 新列的设置
|
||||||
|
* @return {@link SQLMigrate}
|
||||||
|
*/
|
||||||
|
@Contract("null,_,_ -> fail;!null,_,_ -> this")
|
||||||
|
default SQLMigrate column(String oldColumnName, String newColumnName, String newColumnSettings) {
|
||||||
|
return column(oldColumnName, newColumnName, newColumnSettings, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为迁移工作定义前后的列名信息
|
||||||
|
*
|
||||||
|
* @param oldColumnName 旧列名
|
||||||
|
* @param newColumnName 新列名
|
||||||
|
* @param newColumnSettings 新列的设置
|
||||||
|
* @param indexType 索引类型
|
||||||
|
* @return {@link SQLMigrate}
|
||||||
|
*/
|
||||||
|
@Contract("null,_,_,_ -> fail;!null,_,_,_ -> this")
|
||||||
|
SQLMigrate column(String oldColumnName, String newColumnName, String newColumnSettings, IndexType indexType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为迁移工作定义前后的自增列名信息
|
||||||
|
*
|
||||||
|
* @param oldColumnName 旧列名
|
||||||
|
* @param newColumnName 新列名
|
||||||
|
* @return {@link SQLMigrate}
|
||||||
|
*/
|
||||||
|
@Contract("null,_->fail;!null,_ -> this")
|
||||||
|
SQLMigrate autoIncrementColumn(String oldColumnName, String newColumnName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移数据
|
||||||
|
*
|
||||||
|
* @return {@link MigrateResult}
|
||||||
|
*/
|
||||||
|
MigrateResult migrate() throws SQLException;
|
||||||
|
|
||||||
|
}
|
@ -56,6 +56,8 @@ public class EasySQLTest {
|
|||||||
tests.add(new TableMetadataTest());
|
tests.add(new TableMetadataTest());
|
||||||
// tests.add(new DeleteTest());
|
// tests.add(new DeleteTest());
|
||||||
|
|
||||||
|
tests.add(new TableMigrateTest());
|
||||||
|
|
||||||
print("准备进行测试...");
|
print("准备进行测试...");
|
||||||
|
|
||||||
int index = 1;
|
int index = 1;
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package cc.carm.lib.easysql.tests;
|
||||||
|
|
||||||
|
import cc.carm.lib.easysql.TestHandler;
|
||||||
|
import cc.carm.lib.easysql.api.SQLManager;
|
||||||
|
import cc.carm.lib.easysql.api.SQLQuery;
|
||||||
|
import cc.carm.lib.easysql.api.enums.IndexType;
|
||||||
|
import cc.carm.lib.easysql.api.enums.MigrateResult;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2022/11/28<br>
|
||||||
|
* EasySQL<br>
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
*/
|
||||||
|
public class TableMigrateTest extends TestHandler {
|
||||||
|
@Override
|
||||||
|
public void onTest(SQLManager sqlManager) throws Exception {
|
||||||
|
print(" 创建 test_migrate_table");
|
||||||
|
sqlManager.createTable("test_migrate_table")
|
||||||
|
.addAutoIncrementColumn("id")
|
||||||
|
.addColumn("uuid", "VARCHAR(36) NOT NULL", "用户UUID")
|
||||||
|
.addColumn("username", "VARCHAR(16) NOT NULL", "用户名")
|
||||||
|
.addColumn("age", "TINYINT DEFAULT 0 NOT NULL", "年龄")
|
||||||
|
|
||||||
|
.setIndex("uuid", IndexType.UNIQUE_KEY)
|
||||||
|
.build().execute();
|
||||||
|
|
||||||
|
print(" 向 test_migrate_table 写入数据");
|
||||||
|
sqlManager.createInsert("test_migrate_table")
|
||||||
|
.setColumnNames("uuid", "username", "age")
|
||||||
|
.setParams("1234567890", "qwq", 18)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
print(" 迁移 test_migrate_table 至 test_transfer_table");
|
||||||
|
MigrateResult migrate = sqlManager.createMigrate()
|
||||||
|
.tableName("test_migrate_table", "test_transfer_table", null)
|
||||||
|
.autoIncrementColumn("id", "dbId")
|
||||||
|
.column("uuid", "uniqueId", "VARCHAR(40) NOT NULL")
|
||||||
|
.column("username", "name", "VARCHAR(20) NOT NULL")
|
||||||
|
.column("age", "age", "TINYINT DEFAULT 0 NOT NULL")
|
||||||
|
.migrate();
|
||||||
|
|
||||||
|
if (migrate.error() != null) {
|
||||||
|
migrate.error().printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> set = sqlManager.fetchTableMetadata("test_transfer_table").listColumns().join()
|
||||||
|
.stream()
|
||||||
|
.map(String::toLowerCase)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
print(" 迁移后的表结构: " + set);
|
||||||
|
boolean containsAll = set.containsAll(Stream.of
|
||||||
|
("dbId", "uniqueId", "name", "age").map(String::toLowerCase).collect(Collectors.toSet()));
|
||||||
|
print(" 迁移结果: " + (migrate.success() ? "true" : migrate.error()) + " , 是否包含所有字段: " + containsAll);
|
||||||
|
try (SQLQuery query = sqlManager.createQuery().inTable("test_transfer_table").build().execute()) {
|
||||||
|
while (query.getResultSet().next()) {
|
||||||
|
print(" 迁移后的数据: " +
|
||||||
|
query.getResultSet().getInt("dbId") + " , " +
|
||||||
|
query.getResultSet().getString("uniqueId") + " , " +
|
||||||
|
query.getResultSet().getString("name") + " , " +
|
||||||
|
query.getResultSet().getInt("age")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,9 @@ import cc.carm.lib.easysql.api.function.SQLBiFunction;
|
|||||||
import cc.carm.lib.easysql.api.function.SQLDebugHandler;
|
import cc.carm.lib.easysql.api.function.SQLDebugHandler;
|
||||||
import cc.carm.lib.easysql.api.function.SQLExceptionHandler;
|
import cc.carm.lib.easysql.api.function.SQLExceptionHandler;
|
||||||
import cc.carm.lib.easysql.api.function.SQLFunction;
|
import cc.carm.lib.easysql.api.function.SQLFunction;
|
||||||
|
import cc.carm.lib.easysql.api.migrate.SQLMigrate;
|
||||||
import cc.carm.lib.easysql.builder.impl.*;
|
import cc.carm.lib.easysql.builder.impl.*;
|
||||||
|
import cc.carm.lib.easysql.migrate.SQLMigrateImpl;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -199,6 +201,11 @@ public class SQLManagerImpl implements SQLManager {
|
|||||||
return new QueryBuilderImpl(this);
|
return new QueryBuilderImpl(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLMigrate createMigrate() {
|
||||||
|
return new SQLMigrateImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InsertBuilder<PreparedSQLUpdateBatchAction<Integer>> createInsertBatch(@NotNull String tableName) {
|
public InsertBuilder<PreparedSQLUpdateBatchAction<Integer>> createInsertBatch(@NotNull String tableName) {
|
||||||
return new InsertBuilderImpl<PreparedSQLUpdateBatchAction<Integer>>(this, tableName) {
|
return new InsertBuilderImpl<PreparedSQLUpdateBatchAction<Integer>>(this, tableName) {
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package cc.carm.lib.easysql.migrate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2022/11/28<br>
|
||||||
|
* EasySQL<br>
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
*/
|
||||||
|
public class AutoIncrementMigrateData extends MigrateData {
|
||||||
|
public AutoIncrementMigrateData(String name) {
|
||||||
|
super(name, null, null);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package cc.carm.lib.easysql.migrate;
|
||||||
|
|
||||||
|
import cc.carm.lib.easysql.api.enums.IndexType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2022/11/28<br>
|
||||||
|
* EasySQL<br>
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
*/
|
||||||
|
public class MigrateData {
|
||||||
|
private final String name;
|
||||||
|
private final String settings;
|
||||||
|
private final IndexType indexType;
|
||||||
|
|
||||||
|
public MigrateData(String name, String settings, IndexType indexType) {
|
||||||
|
this.name = name;
|
||||||
|
this.settings = settings;
|
||||||
|
this.indexType = indexType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String settings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexType indexType() {
|
||||||
|
return indexType;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
package cc.carm.lib.easysql.migrate;
|
||||||
|
|
||||||
|
import cc.carm.lib.easysql.api.SQLManager;
|
||||||
|
import cc.carm.lib.easysql.api.SQLQuery;
|
||||||
|
import cc.carm.lib.easysql.api.action.PreparedSQLUpdateAction;
|
||||||
|
import cc.carm.lib.easysql.api.builder.TableCreateBuilder;
|
||||||
|
import cc.carm.lib.easysql.api.enums.IndexType;
|
||||||
|
import cc.carm.lib.easysql.api.enums.MigrateResult;
|
||||||
|
import cc.carm.lib.easysql.api.migrate.SQLMigrate;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2022/11/28<br>
|
||||||
|
* EasySQL<br>
|
||||||
|
*
|
||||||
|
* @author huanmeng_qwq
|
||||||
|
*/
|
||||||
|
public class SQLMigrateImpl implements SQLMigrate {
|
||||||
|
private final SQLManager sqlManager;
|
||||||
|
|
||||||
|
private final Map<String, MigrateData> columnMap;
|
||||||
|
|
||||||
|
private String oldTableName;
|
||||||
|
private MigrateData newTableData;
|
||||||
|
|
||||||
|
public SQLMigrateImpl(SQLManager sqlManager) {
|
||||||
|
this.sqlManager = sqlManager;
|
||||||
|
this.columnMap = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLMigrate tableName(String oldTableName, String newTableName, String newTableSettings) {
|
||||||
|
if (oldTableName == null) {
|
||||||
|
throw new IllegalArgumentException("oldTableName can not be null");
|
||||||
|
}
|
||||||
|
this.oldTableName = oldTableName;
|
||||||
|
this.newTableData = new MigrateData(newTableName, newTableSettings, null);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLMigrate column(String oldColumnName, String newColumnName, String newColumnSettings, IndexType indexType) {
|
||||||
|
if (oldColumnName == null) {
|
||||||
|
throw new IllegalArgumentException("oldColumnName can not be null");
|
||||||
|
}
|
||||||
|
columnMap.put(oldColumnName, new MigrateData(newColumnName, newColumnSettings, indexType));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLMigrate autoIncrementColumn(String oldColumnName, String newColumnName) {
|
||||||
|
if (oldColumnName == null) {
|
||||||
|
throw new IllegalArgumentException("oldColumnName can not be null");
|
||||||
|
}
|
||||||
|
columnMap.put(oldColumnName, new AutoIncrementMigrateData(newColumnName));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MigrateResult migrate() throws SQLException {
|
||||||
|
if (oldTableName == null) {
|
||||||
|
return MigrateResult.from(MigrateResult.OLD_TABLE_NOT_EXIST, new IllegalArgumentException("oldTableName can not be null"));
|
||||||
|
}
|
||||||
|
if (newTableData == null) {
|
||||||
|
return MigrateResult.from(MigrateResult.NEW_TABLE_EXIST, new IllegalArgumentException("new table name can not be null"));
|
||||||
|
}
|
||||||
|
if (columnMap.isEmpty()) {
|
||||||
|
return MigrateResult.from(MigrateResult.NEW_COLUMN_EMPTY, new IllegalArgumentException("new column can not be empty"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// check table
|
||||||
|
Set<String> columns = sqlManager.fetchTableMetadata(oldTableName).listColumns().join();
|
||||||
|
for (String column : columns) {
|
||||||
|
if (columnMap.keySet().stream().noneMatch(k -> k.toLowerCase(Locale.ROOT).equals(column.toLowerCase(Locale.ROOT)))) {
|
||||||
|
return MigrateResult.from(MigrateResult.COLUMN_NOT_SET, new IllegalArgumentException("column " + column + " not set"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create new table
|
||||||
|
TableCreateBuilder table = sqlManager.createTable(newTableData.name());
|
||||||
|
if (newTableData.settings() != null) {
|
||||||
|
table.setTableSettings(newTableData.settings());
|
||||||
|
}
|
||||||
|
// add columns
|
||||||
|
for (Map.Entry<String, MigrateData> entry : columnMap.entrySet()) {
|
||||||
|
MigrateData migrateData = entry.getValue();
|
||||||
|
if (migrateData.name() == null) {
|
||||||
|
// ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (migrateData instanceof AutoIncrementMigrateData) {
|
||||||
|
table.addAutoIncrementColumn(migrateData.name());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
table.addColumn(migrateData.name(), migrateData.settings());
|
||||||
|
if (migrateData.indexType() != null) {
|
||||||
|
table.setIndex(migrateData.name(), migrateData.indexType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.build().execute();
|
||||||
|
|
||||||
|
// insert data
|
||||||
|
try (SQLQuery query = sqlManager.createQuery().inTable(oldTableName).build().execute()) {
|
||||||
|
insert(query.getResultSet());
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return new MigrateResult(false, e);
|
||||||
|
}
|
||||||
|
return MigrateResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insert(ResultSet resultSet) throws SQLException {
|
||||||
|
while (resultSet.next()) {
|
||||||
|
Map<String, String> columnNames = tableColumnNames();
|
||||||
|
PreparedSQLUpdateAction<Integer> updateAction = sqlManager.createInsert(newTableData.name()).setColumnNames(columnNames.values().toArray(new String[0]));
|
||||||
|
Object[] values = new Object[columnNames.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (Map.Entry<String, String> entry : columnNames.entrySet()) {
|
||||||
|
values[i] = resultSet.getObject(entry.getKey());
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
updateAction.setParams(values);
|
||||||
|
updateAction.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> tableColumnNames() {
|
||||||
|
Map<String, String> map = new ConcurrentHashMap<>();
|
||||||
|
for (Map.Entry<String, MigrateData> entry : columnMap.entrySet()) {
|
||||||
|
MigrateData migrateData = entry.getValue();
|
||||||
|
if (migrateData.name() == null) {
|
||||||
|
// ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (migrateData instanceof AutoIncrementMigrateData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.put(entry.getKey(), migrateData.name());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user