提交 3a4a3dbf authored 作者: Niklas Mehner's avatar Niklas Mehner

Implemented:

* "Create or replace" for synonyms
* Meta-Data support
* "Drop synonym" statement
* Support for synonyms in different schema
* added more tests.
上级 0e36eb37
...@@ -461,6 +461,11 @@ public interface CommandInterface { ...@@ -461,6 +461,11 @@ public interface CommandInterface {
*/ */
int CREATE_SYNONYM = 86; int CREATE_SYNONYM = 86;
/**
* The type of a DROP SYNONYM statement.
*/
int DROP_SYNONYM = 87;
/** /**
* Get command type. * Get command type.
......
...@@ -1469,6 +1469,14 @@ public class Parser { ...@@ -1469,6 +1469,14 @@ public class Parser {
return parseDropUserDataType(); return parseDropUserDataType();
} else if (readIf("AGGREGATE")) { } else if (readIf("AGGREGATE")) {
return parseDropAggregate(); return parseDropAggregate();
} else if (readIf("SYNONYM")) {
boolean ifExists = readIfExists(false);
String synonymName = readIdentifierWithSchema();
DropSynonym command = new DropSynonym(session, getSchema());
command.setSynonymName(synonymName);
ifExists = readIfExists(ifExists);
command.setIfExists(ifExists);
return command;
} }
throw getSyntaxError(); throw getSyntaxError();
} }
...@@ -4227,7 +4235,7 @@ public class Parser { ...@@ -4227,7 +4235,7 @@ public class Parser {
} }
return parseCreateTable(false, false, cached); return parseCreateTable(false, false, cached);
} else if (readIf("SYNONYM")) { } else if (readIf("SYNONYM")) {
return parseCreateSynonym(); return parseCreateSynonym(orReplace);
} else { } else {
boolean hash = false, primaryKey = false; boolean hash = false, primaryKey = false;
boolean unique = false, spatial = false; boolean unique = false, spatial = false;
...@@ -6058,19 +6066,21 @@ public class Parser { ...@@ -6058,19 +6066,21 @@ public class Parser {
return command; return command;
} }
private CreateSynonym parseCreateSynonym() { private CreateSynonym parseCreateSynonym(boolean orReplace) {
boolean ifNotExists = readIfNoExists(); boolean ifNotExists = readIfNoExists();
String name = readIdentifierWithSchema(); String name = readIdentifierWithSchema();
Schema synonymSchema = getSchema();
read("FOR"); read("FOR");
String tableName = readIdentifierWithSchema(); String tableName = readIdentifierWithSchema();
Schema schema = getSchema(); Schema targetSchema = getSchema();
CreateSynonym command = new CreateSynonym(session, schema); CreateSynonym command = new CreateSynonym(session, synonymSchema);
command.setName(name); command.setName(name);
command.setSynonymFor(tableName); command.setSynonymFor(tableName);
command.setSynonymForSchema(targetSchema);
command.setComment(readCommentIf()); command.setComment(readCommentIf());
command.setIfNotExists(ifNotExists); command.setIfNotExists(ifNotExists);
command.setOrReplace(orReplace);
return command; return command;
} }
......
...@@ -26,6 +26,7 @@ public class CreateSynonym extends SchemaCommand { ...@@ -26,6 +26,7 @@ public class CreateSynonym extends SchemaCommand {
private final CreateSynonymData data = new CreateSynonymData(); private final CreateSynonymData data = new CreateSynonymData();
private boolean ifNotExists; private boolean ifNotExists;
private boolean orReplace;
private String comment; private String comment;
public CreateSynonym(Session session, Schema schema) { public CreateSynonym(Session session, Schema schema) {
...@@ -40,68 +41,64 @@ public class CreateSynonym extends SchemaCommand { ...@@ -40,68 +41,64 @@ public class CreateSynonym extends SchemaCommand {
data.synonymFor = tableName; data.synonymFor = tableName;
} }
public void setSynonymForSchema(Schema synonymForSchema) {
data.synonymForSchema = synonymForSchema;
}
public void setIfNotExists(boolean ifNotExists) { public void setIfNotExists(boolean ifNotExists) {
this.ifNotExists = ifNotExists; this.ifNotExists = ifNotExists;
} }
public void setOrReplace(boolean orReplace) { this.orReplace = orReplace; }
@Override @Override
public int update() { public int update() {
if (!transactional) { if (!transactional) {
session.commit(true); session.commit(true);
} }
Database db = session.getDatabase(); Database db = session.getDatabase();
data.session = session;
// TODO: Check when/if meta data is unlocked... // TODO: Check when/if meta data is unlocked...
db.lockMeta(session); db.lockMeta(session);
if (getSchema().findTableOrView(session, data.synonymName) != null) { Table old = getSchema().findTableOrView(session, data.synonymName);
if (ifNotExists) { if (old != null) {
if (orReplace && old instanceof TableSynonym) {
// ok, we replacing the existing synonym
} else if (ifNotExists && old instanceof TableSynonym) {
return 0; return 0;
} else {
throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1, data.synonymName);
} }
throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1, data.synonymName);
} }
data.id = getObjectId(); validateBackingTableExists();
data.session = session;
TableSynonym table = getSchema().createSynonym(data); TableSynonym table;
table.setComment(comment);
if (old != null) {
db.addSchemaObject(session, table); table = (TableSynonym) old;
data.schema = table.getSchema();
try { table.updateData(data);
HashSet<DbObject> set = New.hashSet(); table.setModified();
set.clear(); } else {
table.addDependencies(set); data.id = getObjectId();
for (DbObject obj : set) { table = getSchema().createSynonym(data);
if (obj == table) { table.setComment(comment);
continue;
} db.addSchemaObject(session, table);
if (obj.getType() == DbObject.TABLE_OR_VIEW) {
if (obj instanceof Table) {
Table t = (Table) obj;
if (t.getId() > table.getId()) {
throw DbException.get(
ErrorCode.FEATURE_NOT_SUPPORTED_1,
"TableSynonym depends on another table " +
"with a higher ID: " + t +
", this is currently not supported, " +
"as it would prevent the database from " +
"being re-opened");
}
}
}
}
} catch (DbException e) {
db.checkPowerOff();
db.removeSchemaObject(session, table);
if (!transactional) {
session.commit(true);
}
throw e;
} }
return 0; return 0;
} }
private void validateBackingTableExists() {
// This call throws an exception if the table does not exist.
if (data.synonymForSchema.findTableOrView(session, data.synonymFor) == null) {
throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1,
data.synonymForSchema.getName() + "." + data.synonymFor);
}
}
public void setComment(String comment) { public void setComment(String comment) {
this.comment = comment; this.comment = comment;
} }
...@@ -111,4 +108,5 @@ public class CreateSynonym extends SchemaCommand { ...@@ -111,4 +108,5 @@ public class CreateSynonym extends SchemaCommand {
return CommandInterface.CREATE_SYNONYM; return CommandInterface.CREATE_SYNONYM;
} }
} }
...@@ -28,6 +28,9 @@ public class CreateSynonymData { ...@@ -28,6 +28,9 @@ public class CreateSynonymData {
*/ */
public String synonymFor; public String synonymFor;
/** Schema synonymFor is located in. */
public Schema synonymForSchema;
/** /**
* The object id. * The object id.
*/ */
...@@ -38,5 +41,4 @@ public class CreateSynonymData { ...@@ -38,5 +41,4 @@ public class CreateSynonymData {
*/ */
public Session session; public Session session;
} }
...@@ -108,7 +108,8 @@ public class MetaTable extends Table { ...@@ -108,7 +108,8 @@ public class MetaTable extends Table {
private static final int LOCKS = 26; private static final int LOCKS = 26;
private static final int SESSION_STATE = 27; private static final int SESSION_STATE = 27;
private static final int QUERY_STATISTICS = 28; private static final int QUERY_STATISTICS = 28;
private static final int META_TABLE_TYPE_COUNT = QUERY_STATISTICS + 1; private static final int SYNONYMS = 29;
private static final int META_TABLE_TYPE_COUNT = SYNONYMS + 1;
private final int type; private final int type;
private final int indexColumn; private final int indexColumn;
...@@ -537,6 +538,20 @@ public class MetaTable extends Table { ...@@ -537,6 +538,20 @@ public class MetaTable extends Table {
); );
break; break;
} }
case SYNONYMS: {
setObjectName("SYNONYMS");
cols = createColumns(
"SYNONYM_CATALOG",
"SYNONYM_SCHEMA",
"SYNONYM_NAME",
"SYNONYM_FOR",
"STATUS",
"REMARKS",
"ID INT"
);
indexColumnName = "SYNONYM_NAME";
break;
}
default: default:
throw DbException.throwInternalError("type="+type); throw DbException.throwInternalError("type="+type);
} }
...@@ -1858,6 +1873,32 @@ public class MetaTable extends Table { ...@@ -1858,6 +1873,32 @@ public class MetaTable extends Table {
} }
break; break;
} }
case SYNONYMS: {
for (Table table : getAllTables(session)) {
if (!table.getTableType().equals(Table.SYNONYM)) {
continue;
}
String synonymName = identifier(table.getName());
TableSynonym synonym = (TableSynonym) table;
add(rows,
// SYNONYM_CATALOG
catalog,
// SYNONYM_SCHEMA
identifier(table.getSchema().getName()),
// SYNONYM_NAME
synonymName,
// SYNONYM_FOR
synonym.getSynonymForName(),
// STATUS
synonym.isInvalid() ? "INVALID" : "VALID",
// REMARKS
replaceNullWithEmpty(synonym.getComment()),
// ID
"" + synonym.getId()
);
}
break;
}
default: default:
DbException.throwInternalError("type="+type); DbException.throwInternalError("type="+type);
} }
......
...@@ -17,36 +17,45 @@ import java.util.HashSet; ...@@ -17,36 +17,45 @@ import java.util.HashSet;
*/ */
public class TableSynonym extends Table { public class TableSynonym extends Table {
private final Table synonymFor; private CreateSynonymData data;
public TableSynonym(CreateSynonymData data) { public TableSynonym(CreateSynonymData data) {
super(data.schema, data.id, data.synonymName, false, false); super(data.schema, data.id, data.synonymName, false, false);
this.synonymFor = data.schema.getTableOrView(data.session, data.synonymFor); this.data = data;
}
public void updateData(CreateSynonymData data) {
this.data = data;
}
private Table getSynonymFor() {
return data.synonymForSchema.getTableOrView(data.session, data.synonymFor);
} }
@Override @Override
public void addDependencies(HashSet<DbObject> dependencies) { public void addDependencies(HashSet<DbObject> dependencies) {
dependencies.add(synonymFor); // no dependency. A table synonym will not prevent the backing table from being dropped, but
// will become invalid instead.
} }
@Override @Override
public Column[] getColumns() { public Column[] getColumns() {
return synonymFor.getColumns(); return getSynonymFor().getColumns();
} }
@Override @Override
public boolean lock(Session session, boolean exclusive, boolean forceLockEvenInMvcc) { public boolean lock(Session session, boolean exclusive, boolean forceLockEvenInMvcc) {
return synonymFor.lock(session, exclusive, forceLockEvenInMvcc); return getSynonymFor().lock(session, exclusive, forceLockEvenInMvcc);
} }
@Override @Override
public void close(Session session) { public void close(Session session) {
synonymFor.close(session); getSynonymFor().close(session);
} }
@Override @Override
public void unlock(Session s) { public void unlock(Session s) {
synonymFor.unlock(s); getSynonymFor().unlock(s);
} }
@Override @Override
...@@ -56,17 +65,17 @@ public class TableSynonym extends Table { ...@@ -56,17 +65,17 @@ public class TableSynonym extends Table {
@Override @Override
public void removeRow(Session session, Row row) { public void removeRow(Session session, Row row) {
synonymFor.removeRow(session, row); getSynonymFor().removeRow(session, row);
} }
@Override @Override
public void truncate(Session session) { public void truncate(Session session) {
synonymFor.truncate(session); getSynonymFor().truncate(session);
} }
@Override @Override
public void addRow(Session session, Row row) { public void addRow(Session session, Row row) {
synonymFor.addRow(session, row); getSynonymFor().addRow(session, row);
} }
@Override @Override
...@@ -81,37 +90,37 @@ public class TableSynonym extends Table { ...@@ -81,37 +90,37 @@ public class TableSynonym extends Table {
@Override @Override
public Index getScanIndex(Session session) { public Index getScanIndex(Session session) {
return synonymFor.getScanIndex(session); return getSynonymFor().getScanIndex(session);
} }
@Override @Override
public Index getUniqueIndex() { public Index getUniqueIndex() {
return synonymFor.getUniqueIndex(); return getSynonymFor().getUniqueIndex();
} }
@Override @Override
public ArrayList<Index> getIndexes() { public ArrayList<Index> getIndexes() {
return synonymFor.getIndexes(); return getSynonymFor().getIndexes();
} }
@Override @Override
public boolean isLockedExclusively() { public boolean isLockedExclusively() {
return synonymFor.isLockedExclusively(); return getSynonymFor().isLockedExclusively();
} }
@Override @Override
public long getMaxDataModificationId() { public long getMaxDataModificationId() {
return synonymFor.getMaxDataModificationId(); return getSynonymFor().getMaxDataModificationId();
} }
@Override @Override
public boolean isDeterministic() { public boolean isDeterministic() {
return synonymFor.isDeterministic(); return getSynonymFor().isDeterministic();
} }
@Override @Override
public boolean canGetRowCount() { public boolean canGetRowCount() {
return synonymFor.canGetRowCount(); return getSynonymFor().canGetRowCount();
} }
@Override @Override
...@@ -121,27 +130,27 @@ public class TableSynonym extends Table { ...@@ -121,27 +130,27 @@ public class TableSynonym extends Table {
@Override @Override
public long getRowCount(Session session) { public long getRowCount(Session session) {
return synonymFor.getRowCount(session); return getSynonymFor().getRowCount(session);
} }
@Override @Override
public long getRowCountApproximation() { public long getRowCountApproximation() {
return synonymFor.getRowCountApproximation(); return getSynonymFor().getRowCountApproximation();
} }
@Override @Override
public long getDiskSpaceUsed() { public long getDiskSpaceUsed() {
return synonymFor.getDiskSpaceUsed(); return getSynonymFor().getDiskSpaceUsed();
} }
@Override @Override
public String getCreateSQL() { public String getCreateSQL() {
return "CREATE SYNONYM " + getName() + " FOR " + synonymFor; return "CREATE SYNONYM " + getName() + " FOR " + data.synonymForSchema.getName() + "." + data.synonymFor;
} }
@Override @Override
public String getDropSQL() { public String getDropSQL() {
return "DROP SYNONYM " + getName() + " FOR " + synonymFor; return "DROP SYNONYM " + getName();
} }
@Override @Override
...@@ -150,6 +159,20 @@ public class TableSynonym extends Table { ...@@ -150,6 +159,20 @@ public class TableSynonym extends Table {
} }
public boolean canTruncate() { public boolean canTruncate() {
return synonymFor.canTruncate(); return getSynonymFor().canTruncate();
} }
public String getSynonymForName() {
return data.synonymFor;
}
public boolean isInvalid() {
try {
getSynonymFor();
return false;
} catch (DbException e) {
return true;
}
}
} }
...@@ -5,9 +5,15 @@ ...@@ -5,9 +5,15 @@
*/ */
package org.h2.test.db; package org.h2.test.db;
import org.h2.engine.Constants;
import org.h2.jdbc.JdbcSQLException;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import java.sql.*; import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/** /**
* Test for the read-only database feature. * Test for the read-only database feature.
...@@ -24,18 +30,158 @@ public class TestSynonymForTable extends TestBase { ...@@ -24,18 +30,158 @@ public class TestSynonymForTable extends TestBase {
} }
@Override @Override
public void test() throws Exception { public void test() throws SQLException {
testSelectFromSynonym(); testSelectFromSynonym();
testInsertIntoSynonym(); testInsertIntoSynonym();
testDeleteFromSynonym(); testDeleteFromSynonym();
testTruncateSynonym(); testTruncateSynonym();
// TODO: check create existing tablename testExistingTableName();
// TODO: check create for non existing table testCreateForUnknownTable();
// TODO: Test Meta Data testMetaData();
// TODO: CREATE OR REPLACE TableSynonym. testCreateOrReplace();
// TODO: Check schema name in synonym and table testCreateOrReplaceExistingTable();
testSynonymInDifferentSchema();
testReopenDatabase(); testReopenDatabase();
// TODO: test drop synonym. testDropSynonym();
testDropTable();
testDropSchema();
}
private void testDropSchema() throws SQLException {
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
stat.execute("CREATE SCHEMA IF NOT EXISTS s1");
stat.execute("CREATE TABLE IF NOT EXISTS s1.backingtable(id INT PRIMARY KEY)");
stat.execute("CREATE OR REPLACE SYNONYM testsynonym FOR s1.backingtable");
stat.execute("DROP SCHEMA s1");
assertThrows(JdbcSQLException.class, stat).execute("SELECT id FROM testsynonym");
}
private void testDropTable() throws SQLException {
Connection conn = getConnection("synonym");
createTableWithSynonym(conn);
Statement stat = conn.createStatement();
stat.execute("DROP TABLE backingtable");
// Backing table does not exist anymore.
assertThrows(JdbcSQLException.class, stat).execute("SELECT id FROM testsynonym");
// Meta data should show INVALID
ResultSet synonyms = conn.createStatement().executeQuery("SELECT * FROM INFORMATION_SCHEMA.SYNONYMS WHERE SYNONYM_NAME='TESTSYNONYM'");
assertTrue(synonyms.next());
assertEquals("TESTSYNONYM", synonyms.getString("SYNONYM_NAME"));
assertEquals("INVALID", synonyms.getString("STATUS"));
conn.close();
// Reopending should work with invalid synonym
Connection conn2 = getConnection("synonym");
assertThrows(JdbcSQLException.class, stat).execute("SELECT id FROM testsynonym");
conn2.close();
}
private void testDropSynonym() throws SQLException {
Connection conn = getConnection("synonym");
createTableWithSynonym(conn);
Statement stat = conn.createStatement();
stat.execute("DROP SYNONYM testsynonym");
// Synonym does not exist anymore.
assertThrows(JdbcSQLException.class, stat).execute("SELECT id FROM testsynonym");
// Dropping with "if exists" should succeed even if the synonym does not exist anymore.
stat.execute("DROP SYNONYM IF EXISTS testsynonym");
// Without "if exists" the command should fail.
assertThrows(JdbcSQLException.class, stat).execute("DROP SYNONYM testsynonym");
}
private void testSynonymInDifferentSchema() throws SQLException {
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
stat.execute("CREATE SCHEMA IF NOT EXISTS s1");
stat.execute("CREATE TABLE IF NOT EXISTS s1.backingtable(id INT PRIMARY KEY)");
stat.execute("TRUNCATE TABLE s1.backingtable");
stat.execute("CREATE OR REPLACE SYNONYM testsynonym FOR s1.backingtable");
stat.execute("INSERT INTO s1.backingtable VALUES(15)");
assertSynonymContains(conn, 15);
}
private void testCreateOrReplaceExistingTable() throws SQLException {
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE IF NOT EXISTS backingtable(id INT PRIMARY KEY)");
assertThrows(JdbcSQLException.class, stat).execute("CREATE OR REPLACE SYNONYM backingtable FOR backingtable");
conn.close();
}
private void testCreateOrReplace() throws SQLException {
// start with a fresh db so the first create or replace has to actually create the synonym.
deleteDb("synonym");
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE IF NOT EXISTS backingtable(id INT PRIMARY KEY)");
stat.execute("CREATE TABLE IF NOT EXISTS backingtable2(id INT PRIMARY KEY)");
stat.execute("CREATE OR REPLACE SYNONYM testsynonym FOR backingtable");
insertIntoBackingTable(conn, 17);
ResultSet rs = stat.executeQuery("SELECT id FROM testsynonym");
assertTrue(rs.next());
assertEquals(17, rs.getInt(1));
stat.execute("CREATE OR REPLACE SYNONYM testsynonym FOR backingtable2");
// Should not return a result, since backingtable2 is empty.
ResultSet rs2 = stat.executeQuery("SELECT id FROM testsynonym");
assertFalse(rs2.next());
conn.close();
deleteDb("synonym");
}
private void testMetaData() throws SQLException {
Connection conn = getConnection("synonym");
createTableWithSynonym(conn);
ResultSet tables = conn.getMetaData().getTables(null, Constants.SCHEMA_MAIN, null,
new String[]{"SYNONYM"});
assertTrue(tables.next());
assertEquals(tables.getString("TABLE_NAME"), "TESTSYNONYM");
assertEquals(tables.getString("TABLE_TYPE"), "SYNONYM");
assertFalse(tables.next());
ResultSet synonyms = conn.createStatement().executeQuery("SELECT * FROM INFORMATION_SCHEMA.SYNONYMS");
assertTrue(synonyms.next());
assertEquals("SYNONYM", synonyms.getString("SYNONYM_CATALOG"));
assertEquals("PUBLIC", synonyms.getString("SYNONYM_SCHEMA"));
assertEquals("TESTSYNONYM", synonyms.getString("SYNONYM_NAME"));
assertEquals("BACKINGTABLE", synonyms.getString("SYNONYM_FOR"));
assertEquals("VALID", synonyms.getString("STATUS"));
assertEquals("", synonyms.getString("REMARKS"));
assertTrue(synonyms.getString("ID") != null);
assertFalse(synonyms.next());
conn.close();
}
private void testCreateForUnknownTable() throws SQLException {
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
assertThrows(JdbcSQLException.class, stat).execute("CREATE SYNONYM someSynonym FOR nonexistingTable");
conn.close();
}
private void testExistingTableName() throws SQLException {
Connection conn = getConnection("synonym");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE IF NOT EXISTS backingtable(id INT PRIMARY KEY)");
assertThrows(JdbcSQLException.class, stat).execute("CREATE SYNONYM backingtable FOR backingtable");
conn.close();
} }
/** /**
...@@ -125,14 +271,14 @@ public class TestSynonymForTable extends TestBase { ...@@ -125,14 +271,14 @@ public class TestSynonymForTable extends TestBase {
private void insertIntoSynonym(Connection conn, int id) throws SQLException { private void insertIntoSynonym(Connection conn, int id) throws SQLException {
PreparedStatement prep = conn.prepareStatement( PreparedStatement prep = conn.prepareStatement(
"insert into testsynonym values(?)"); "INSERT INTO testsynonym VALUES(?)");
prep.setInt(1, id); prep.setInt(1, id);
prep.execute(); prep.execute();
} }
private void insertIntoBackingTable(Connection conn, int id) throws SQLException { private void insertIntoBackingTable(Connection conn, int id) throws SQLException {
PreparedStatement prep = conn.prepareStatement( PreparedStatement prep = conn.prepareStatement(
"insert into backingtable values(?)"); "INSERT INTO backingtable VALUES(?)");
prep.setInt(1, id); prep.setInt(1, id);
prep.execute(); prep.execute();
} }
...@@ -140,7 +286,7 @@ public class TestSynonymForTable extends TestBase { ...@@ -140,7 +286,7 @@ public class TestSynonymForTable extends TestBase {
private void createTableWithSynonym(Connection conn) throws SQLException { private void createTableWithSynonym(Connection conn) throws SQLException {
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
stat.execute("CREATE TABLE IF NOT EXISTS backingtable(id INT PRIMARY KEY)"); stat.execute("CREATE TABLE IF NOT EXISTS backingtable(id INT PRIMARY KEY)");
stat.execute("CREATE SYNONYM IF NOT EXISTS testsynonym FOR backingtable"); stat.execute("CREATE OR REPLACE SYNONYM testsynonym FOR backingtable");
stat.execute("TRUNCATE TABLE backingtable"); stat.execute("TRUNCATE TABLE backingtable");
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论