Unverified 提交 c877fe32 authored 作者: Noel Grandin's avatar Noel Grandin 提交者: GitHub

Merge pull request #646 from stumc/Issue#645

Issue#646 NPE in CREATE VIEW WITH RECURSIVE & NON_RECURSIVE CTE
...@@ -14,3 +14,4 @@ test.out.txt ...@@ -14,3 +14,4 @@ test.out.txt
*.log *.log
target/ target/
src/main/org/h2/res/help.csv src/main/org/h2/res/help.csv
_tmp*
...@@ -34,6 +34,7 @@ public class CreateView extends SchemaCommand { ...@@ -34,6 +34,7 @@ public class CreateView extends SchemaCommand {
private String comment; private String comment;
private boolean orReplace; private boolean orReplace;
private boolean force; private boolean force;
private boolean isTableExpression;
public CreateView(Session session, Schema schema) { public CreateView(Session session, Schema schema) {
super(session, schema); super(session, schema);
...@@ -70,6 +71,10 @@ public class CreateView extends SchemaCommand { ...@@ -70,6 +71,10 @@ public class CreateView extends SchemaCommand {
public void setForce(boolean force) { public void setForce(boolean force) {
this.force = force; this.force = force;
} }
public void setTableExpression(boolean isTableExpression) {
this.isTableExpression = isTableExpression;
}
@Override @Override
public int update() { public int update() {
...@@ -98,17 +103,27 @@ public class CreateView extends SchemaCommand { ...@@ -98,17 +103,27 @@ public class CreateView extends SchemaCommand {
} }
querySQL = select.getPlanSQL(); querySQL = select.getPlanSQL();
} }
Column[] columnTemplates = null; Column[] columnTemplatesAsUnknowns = null;
Column[] columnTemplatesAsStrings = null;
if (columnNames != null) { if (columnNames != null) {
columnTemplates = new Column[columnNames.length]; columnTemplatesAsUnknowns = new Column[columnNames.length];
columnTemplatesAsStrings = new Column[columnNames.length];
for (int i = 0; i < columnNames.length; ++i) { for (int i = 0; i < columnNames.length; ++i) {
columnTemplates[i] = new Column(columnNames[i], Value.UNKNOWN); // non table expressions are fine to use unknown column type
columnTemplatesAsUnknowns[i] = new Column(columnNames[i], Value.UNKNOWN);
// table expressions can't have unknown types - so we use string instead
columnTemplatesAsStrings[i] = new Column(columnNames[i], Value.STRING);
} }
} }
if (view == null) { if (view == null) {
view = new TableView(getSchema(), id, viewName, querySQL, null, columnTemplates, session, false, false); if (isTableExpression) {
view = TableView.createTableViewMaybeRecursive(getSchema(), id, viewName, querySQL, null, columnTemplatesAsStrings, session, false /* literalsChecked */, isTableExpression, true /*isPersistent*/, db);
} else {
view = new TableView(getSchema(), id, viewName, querySQL, null, columnTemplatesAsUnknowns, session, false/* allow recursive */, false/* literalsChecked */, isTableExpression, true);
}
} else { } else {
view.replace(querySQL, columnTemplates, session, false, force, false); // TODO support isTableExpression in replace function...
view.replace(querySQL, columnTemplatesAsUnknowns, session, false, force, false);
view.setModified(); view.setModified();
} }
if (comment != null) { if (comment != null) {
...@@ -116,9 +131,14 @@ public class CreateView extends SchemaCommand { ...@@ -116,9 +131,14 @@ public class CreateView extends SchemaCommand {
} }
if (old == null) { if (old == null) {
db.addSchemaObject(session, view); db.addSchemaObject(session, view);
db.unlockMeta(session);
} else { } else {
db.updateMeta(session, view); db.updateMeta(session, view);
} }
// TODO: if we added any table expressions that aren't used by this view, detect them
// and drop them - otherwise they will leak and never get cleaned up.
return 0; return 0;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
*/ */
package org.h2.command.ddl; package org.h2.command.ddl;
import java.util.ArrayList;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.command.CommandInterface; import org.h2.command.CommandInterface;
import org.h2.constraint.ConstraintReferential; import org.h2.constraint.ConstraintReferential;
...@@ -67,9 +68,27 @@ public class DropView extends SchemaCommand { ...@@ -67,9 +68,27 @@ public class DropView extends SchemaCommand {
} }
} }
} }
// TODO: Where is the ConstraintReferential.CASCADE style drop processing ? It's
// supported from imported keys - but not for dependent db objects
TableView tableView = (TableView) view;
ArrayList<Table> copyOfDependencies = new ArrayList<Table>(tableView.getTables());
view.lock(session, true, true); view.lock(session, true, true);
session.getDatabase().removeSchemaObject(session, view); session.getDatabase().removeSchemaObject(session, view);
// remove dependent table expressions
for (Table childTable: copyOfDependencies) {
if (TableType.VIEW == childTable.getTableType()) {
TableView childTableView = (TableView) childTable;
if (childTableView.isTableExpression() && childTableView.getName() != null) {
session.getDatabase().removeSchemaObject(session, childTableView);
}
}
}
// make sure its all unlocked
session.getDatabase().unlockMeta(session);
} }
return 0; return 0;
} }
......
...@@ -53,7 +53,7 @@ public class Delete extends Prepared { ...@@ -53,7 +53,7 @@ public class Delete extends Prepared {
this.condition = condition; this.condition = condition;
} }
public Expression getCondition( ) { public Expression getCondition() {
return this.condition; return this.condition;
} }
...@@ -136,17 +136,16 @@ public class Delete extends Prepared { ...@@ -136,17 +136,16 @@ public class Delete extends Prepared {
public void prepare() { public void prepare() {
if (condition != null) { if (condition != null) {
condition.mapColumns(targetTableFilter, 0); condition.mapColumns(targetTableFilter, 0);
if(sourceTableFilter!=null){ if (sourceTableFilter != null) {
condition.mapColumns(sourceTableFilter, 0); condition.mapColumns(sourceTableFilter, 0);
} }
condition = condition.optimize(session); condition = condition.optimize(session);
condition.createIndexConditions(session, targetTableFilter); condition.createIndexConditions(session, targetTableFilter);
} }
TableFilter[] filters; TableFilter[] filters;
if(sourceTableFilter==null){ if (sourceTableFilter == null) {
filters = new TableFilter[] { targetTableFilter }; filters = new TableFilter[] { targetTableFilter };
} } else {
else{
filters = new TableFilter[] { targetTableFilter, sourceTableFilter }; filters = new TableFilter[] { targetTableFilter, sourceTableFilter };
} }
PlanItem item = targetTableFilter.getBestPlanItem(session, filters, 0, PlanItem item = targetTableFilter.getBestPlanItem(session, filters, 0,
......
...@@ -79,7 +79,8 @@ import org.h2.value.Value; ...@@ -79,7 +79,8 @@ import org.h2.value.Value;
* 4) Previously if neither UPDATE or DELETE clause is supplied, but INSERT is supplied - the INSERT * 4) Previously if neither UPDATE or DELETE clause is supplied, but INSERT is supplied - the INSERT
* action is always triggered. This is because the embedded UPDATE and DELETE statement's * action is always triggered. This is because the embedded UPDATE and DELETE statement's
* returned update row count is used to detect a matching join. * returned update row count is used to detect a matching join.
* If neither of the two the statements are provided, no matching join is EVER detected. * If neither of the two the statements are provided, no matching join is NEVER detected.
*
* A fix for this is now implemented as described below: * A fix for this is now implemented as described below:
* We now generate a "matchSelect" query and use that to always detect * We now generate a "matchSelect" query and use that to always detect
* a match join - rather than relying on UPDATE or DELETE statements. * a match join - rather than relying on UPDATE or DELETE statements.
...@@ -111,11 +112,12 @@ public class MergeUsing extends Prepared { ...@@ -111,11 +112,12 @@ public class MergeUsing extends Prepared {
private Delete deleteCommand; private Delete deleteCommand;
private Insert insertCommand; private Insert insertCommand;
private String queryAlias; private String queryAlias;
private int countUpdatedRows = 0; private int countUpdatedRows;
private Column[] sourceKeys; private Column[] sourceKeys;
private Select targetMatchQuery; private Select targetMatchQuery;
private final HashMap<Value, Integer> targetRowidsRemembered = new HashMap<>(); private final HashMap<Value, Integer> targetRowidsRemembered = new HashMap<>();
private int sourceQueryRowNumber = 0; private int sourceQueryRowNumber;
public MergeUsing(Merge merge) { public MergeUsing(Merge merge) {
super(merge.getSession()); super(merge.getSession());
......
...@@ -817,14 +817,14 @@ public class Select extends Query { ...@@ -817,14 +817,14 @@ public class Select extends Query {
sort = prepareOrder(orderList, expressions.size()); sort = prepareOrder(orderList, expressions.size());
orderList = null; orderList = null;
} }
ColumnNamer columnNamer= new ColumnNamer(session); ColumnNamer columnNamer = new ColumnNamer(session);
for (int i = 0; i < expressions.size(); i++) { for (int i = 0; i < expressions.size(); i++) {
Expression e = expressions.get(i); Expression e = expressions.get(i);
String proposedColumnName = e.getAlias(); String proposedColumnName = e.getAlias();
String columnName = columnNamer.getColumnName(e,i,proposedColumnName); String columnName = columnNamer.getColumnName(e, i, proposedColumnName);
// if the name changed, create an alias // if the name changed, create an alias
if(!columnName.equals(proposedColumnName)){ if (!columnName.equals(proposedColumnName)) {
e = new Alias(e,columnName,true); e = new Alias(e, columnName, true);
} }
expressions.set(i, e.optimize(session)); expressions.set(i, e.optimize(session));
} }
...@@ -852,7 +852,7 @@ public class Select extends Query { ...@@ -852,7 +852,7 @@ public class Select extends Query {
isQuickAggregateQuery = isEverything(optimizable); isQuickAggregateQuery = isEverything(optimizable);
} }
} }
cost = preparePlan(session.isParsingView()); cost = preparePlan(session.isParsingCreateView());
if (distinct && session.getDatabase().getSettings().optimizeDistinct && if (distinct && session.getDatabase().getSettings().optimizeDistinct &&
!isGroupQuery && filters.size() == 1 && !isGroupQuery && filters.size() == 1 &&
expressions.size() == 1 && condition == null) { expressions.size() == 1 && condition == null) {
...@@ -1060,14 +1060,22 @@ public class Select extends Query { ...@@ -1060,14 +1060,22 @@ public class Select extends Query {
StatementBuilder buff = new StatementBuilder(); StatementBuilder buff = new StatementBuilder();
for (TableFilter f : topFilters) { for (TableFilter f : topFilters) {
Table t = f.getTable(); Table t = f.getTable();
if (t.isView() && ((TableView) t).isRecursive()) { TableView tableView = t.isView() ? (TableView) t : null;
buff.append("WITH RECURSIVE ").append(t.getName()).append('('); if (tableView != null && tableView.isRecursive() && tableView.isTableExpression()) {
buff.resetCount();
for (Column c : t.getColumns()) { if (tableView.isPersistent()) {
buff.appendExceptFirst(","); // skip the generation of plan SQL for this already recursive persistent ctes, since using a with
buff.append(c.getName()); // statement will re-create the common table expression views.
continue;
} else {
buff.append("WITH RECURSIVE ").append(t.getName()).append('(');
buff.resetCount();
for (Column c : t.getColumns()) {
buff.appendExceptFirst(",");
buff.append(c.getName());
}
buff.append(") AS ").append(t.getSQL()).append("\n");
} }
buff.append(") AS ").append(t.getSQL()).append("\n");
} }
} }
buff.resetCount(); buff.resetCount();
......
...@@ -90,6 +90,9 @@ import org.h2.value.ValueInt; ...@@ -90,6 +90,9 @@ import org.h2.value.ValueInt;
public class Database implements DataHandler { public class Database implements DataHandler {
private static int initialPowerOffCount; private static int initialPowerOffCount;
private static final ThreadLocal<Session> META_LOCK_DEBUGGING = new ThreadLocal<Session>();
private static final ThreadLocal<Throwable> META_LOCK_DEBUGGING_STACK = new ThreadLocal<Throwable>();
/** /**
* The default name of the system user. This name is only used as long as * The default name of the system user. This name is only used as long as
...@@ -296,7 +299,7 @@ public class Database implements DataHandler { ...@@ -296,7 +299,7 @@ public class Database implements DataHandler {
e.fillInStackTrace(); e.fillInStackTrace();
} }
boolean alreadyOpen = e instanceof DbException boolean alreadyOpen = e instanceof DbException
&& ((DbException)e).getErrorCode() == ErrorCode.DATABASE_ALREADY_OPEN_1; && ((DbException) e).getErrorCode() == ErrorCode.DATABASE_ALREADY_OPEN_1;
if (alreadyOpen) { if (alreadyOpen) {
stopServer(); stopServer();
} }
...@@ -765,7 +768,7 @@ public class Database implements DataHandler { ...@@ -765,7 +768,7 @@ public class Database implements DataHandler {
Collections.sort(records); Collections.sort(records);
synchronized (systemSession) { synchronized (systemSession) {
for (MetaRecord rec : records) { for (MetaRecord rec : records) {
rec.execute(this, systemSession, eventListener); rec.execute(this, systemSession, eventListener);
} }
} }
if (mvStore != null) { if (mvStore != null) {
...@@ -899,10 +902,7 @@ public class Database implements DataHandler { ...@@ -899,10 +902,7 @@ public class Database implements DataHandler {
} }
} }
private static final ThreadLocal<Session> metaLockDebugging = new ThreadLocal<Session>(); /**
private static final ThreadLocal<Throwable> metaLockDebuggingStack = new ThreadLocal<Throwable>();
/**
* Lock the metadata table for updates. * Lock the metadata table for updates.
* *
* @param session the session * @param session the session
...@@ -917,22 +917,23 @@ public class Database implements DataHandler { ...@@ -917,22 +917,23 @@ public class Database implements DataHandler {
return true; return true;
} }
if (SysProperties.CHECK2) { if (SysProperties.CHECK2) {
final Session prev = metaLockDebugging.get(); final Session prev = META_LOCK_DEBUGGING.get();
if (prev == null) { if (prev == null) {
metaLockDebugging.set(session); META_LOCK_DEBUGGING.set(session);
metaLockDebuggingStack.set(new Throwable()); META_LOCK_DEBUGGING_STACK.set(new Throwable("Last meta lock granted in this stack trace, "+
"this is debug information for following IllegalStateException"));
} else if (prev != session) { } else if (prev != session) {
metaLockDebuggingStack.get().printStackTrace(); META_LOCK_DEBUGGING_STACK.get().printStackTrace();
throw new IllegalStateException("meta currently locked by " throw new IllegalStateException("meta currently locked by "
+ prev + prev +", sessionid="+ prev.getId()
+ " and trying to be locked by different session, " + " and trying to be locked by different session, "
+ session + " on same thread"); + session +", sessionid="+ session.getId() + " on same thread");
} }
} }
boolean wasLocked = meta.lock(session, true, true); boolean wasLocked = meta.lock(session, true, true);
return wasLocked; return wasLocked;
} }
/** /**
* Unlock the metadata table. * Unlock the metadata table.
* *
...@@ -952,9 +953,9 @@ public class Database implements DataHandler { ...@@ -952,9 +953,9 @@ public class Database implements DataHandler {
*/ */
public void unlockMetaDebug(Session session) { public void unlockMetaDebug(Session session) {
if (SysProperties.CHECK2) { if (SysProperties.CHECK2) {
if (metaLockDebugging.get() == session) { if (META_LOCK_DEBUGGING.get() == session) {
metaLockDebugging.set(null); META_LOCK_DEBUGGING.set(null);
metaLockDebuggingStack.set(null); META_LOCK_DEBUGGING_STACK.set(null);
} }
} }
} }
...@@ -1911,13 +1912,14 @@ public class Database implements DataHandler { ...@@ -1911,13 +1912,14 @@ public class Database implements DataHandler {
t.getSQL()); t.getSQL());
} }
obj.removeChildrenAndResources(session); obj.removeChildrenAndResources(session);
} }
removeMeta(session, id); removeMeta(session, id);
} }
} }
/** /**
* Check if this database disk-based. * Check if this database is disk-based.
* *
* @return true if it is disk-based, false it it is in-memory only. * @return true if it is disk-based, false it it is in-memory only.
*/ */
......
...@@ -6,12 +6,14 @@ ...@@ -6,12 +6,14 @@
package org.h2.engine; package org.h2.engine;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.ArrayDeque;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.command.Command; import org.h2.command.Command;
...@@ -121,6 +123,7 @@ public class Session extends SessionWithState { ...@@ -121,6 +123,7 @@ public class Session extends SessionWithState {
private long modificationMetaID = -1; private long modificationMetaID = -1;
private SubQueryInfo subQueryInfo; private SubQueryInfo subQueryInfo;
private int parsingView; private int parsingView;
private Deque<String> viewNameStack = new ArrayDeque<String>();
private int preparingQueryExpression; private int preparingQueryExpression;
private volatile SmallLRUCache<Object, ViewIndex> viewIndexCache; private volatile SmallLRUCache<Object, ViewIndex> viewIndexCache;
private HashMap<Object, ViewIndex> subQueryIndexCache; private HashMap<Object, ViewIndex> subQueryIndexCache;
...@@ -226,13 +229,25 @@ public class Session extends SessionWithState { ...@@ -226,13 +229,25 @@ public class Session extends SessionWithState {
return subQueryInfo; return subQueryInfo;
} }
public void setParsingView(boolean parsingView) { public void setParsingCreateView(boolean parsingView, String viewName) {
// It can be recursive, thus implemented as counter. // It can be recursive, thus implemented as counter.
this.parsingView += parsingView ? 1 : -1; this.parsingView += parsingView ? 1 : -1;
assert this.parsingView >= 0; assert this.parsingView >= 0;
if (parsingView) {
viewNameStack.push(viewName);
} else {
assert viewName.equals(viewNameStack.peek());
viewNameStack.pop();
}
}
public String getParsingCreateViewName() {
if (viewNameStack.size() == 0) {
return null;
}
return viewNameStack.peek();
} }
public boolean isParsingView() { public boolean isParsingCreateView() {
assert parsingView >= 0; assert parsingView >= 0;
return parsingView != 0; return parsingView != 0;
} }
...@@ -679,7 +694,8 @@ public class Session extends SessionWithState { ...@@ -679,7 +694,8 @@ public class Session extends SessionWithState {
for (Table table : tablesToAnalyze) { for (Table table : tablesToAnalyze) {
Analyze.analyzeTable(this, table, rows, false); Analyze.analyzeTable(this, table, rows, false);
} }
database.unlockMeta(this); // analyze can lock the meta // analyze can lock the meta
database.unlockMeta(this);
} }
tablesToAnalyze = null; tablesToAnalyze = null;
} }
......
...@@ -8,7 +8,6 @@ package org.h2.index; ...@@ -8,7 +8,6 @@ package org.h2.index;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.command.Parser; import org.h2.command.Parser;
import org.h2.command.Prepared; import org.h2.command.Prepared;
...@@ -182,10 +181,10 @@ public class ViewIndex extends BaseIndex implements SpatialIndex { ...@@ -182,10 +181,10 @@ public class ViewIndex extends BaseIndex implements SpatialIndex {
private Cursor findRecursive(SearchRow first, SearchRow last) { private Cursor findRecursive(SearchRow first, SearchRow last) {
assert recursive; assert recursive;
ResultInterface recResult = view.getRecursiveResult(); ResultInterface recursiveResult = view.getRecursiveResult();
if (recResult != null) { if (recursiveResult != null) {
recResult.reset(); recursiveResult.reset();
return new ViewCursor(this, recResult, first, last); return new ViewCursor(this, recursiveResult, first, last);
} }
if (query == null) { if (query == null) {
Parser parser = new Parser(createSession); Parser parser = new Parser(createSession);
...@@ -200,35 +199,39 @@ public class ViewIndex extends BaseIndex implements SpatialIndex { ...@@ -200,35 +199,39 @@ public class ViewIndex extends BaseIndex implements SpatialIndex {
} }
SelectUnion union = (SelectUnion) query; SelectUnion union = (SelectUnion) query;
Query left = union.getLeft(); Query left = union.getLeft();
left.setNeverLazy(true);
// to ensure the last result is not closed // to ensure the last result is not closed
left.disableCache(); left.disableCache();
ResultInterface r = left.query(0); ResultInterface resultInterface = left.query(0);
LocalResult result = union.getEmptyResult(); LocalResult localResult = union.getEmptyResult();
// ensure it is not written to disk, // ensure it is not written to disk,
// because it is not closed normally // because it is not closed normally
result.setMaxMemoryRows(Integer.MAX_VALUE); localResult.setMaxMemoryRows(Integer.MAX_VALUE);
while (r.next()) { while (resultInterface.next()) {
result.addRow(r.currentRow()); Value[] cr = resultInterface.currentRow();
localResult.addRow(cr);
} }
Query right = union.getRight(); Query right = union.getRight();
r.reset(); right.setNeverLazy(true);
view.setRecursiveResult(r); resultInterface.reset();
view.setRecursiveResult(resultInterface);
// to ensure the last result is not closed // to ensure the last result is not closed
right.disableCache(); right.disableCache();
while (true) { while (true) {
r = right.query(0); resultInterface = right.query(0);
if (!r.hasNext()) { if (!resultInterface.hasNext()) {
break; break;
} }
while (r.next()) { while (resultInterface.next()) {
result.addRow(r.currentRow()); Value[] cr = resultInterface.currentRow();
localResult.addRow(cr);
} }
r.reset(); resultInterface.reset();
view.setRecursiveResult(r); view.setRecursiveResult(resultInterface);
} }
view.setRecursiveResult(null); view.setRecursiveResult(null);
result.done(); localResult.done();
return new ViewCursor(this, result, first, last); return new ViewCursor(this, localResult, first, last);
} }
/** /**
......
...@@ -48,7 +48,6 @@ import org.h2.value.Value; ...@@ -48,7 +48,6 @@ import org.h2.value.Value;
* A table stored in a MVStore. * A table stored in a MVStore.
*/ */
public class MVTable extends TableBase { public class MVTable extends TableBase {
/** /**
* The table name this thread is waiting to lock. * The table name this thread is waiting to lock.
*/ */
...@@ -63,7 +62,32 @@ public class MVTable extends TableBase { ...@@ -63,7 +62,32 @@ public class MVTable extends TableBase {
* The tables names this thread has a shared lock on. * The tables names this thread has a shared lock on.
*/ */
public static final DebuggingThreadLocal<ArrayList<String>> SHARED_LOCKS; public static final DebuggingThreadLocal<ArrayList<String>> SHARED_LOCKS;
/**
* The type of trace lock events
*/
private enum TraceLockEvent{
TRACE_LOCK_OK("ok"),
TRACE_LOCK_WAITING_FOR("waiting for"),
TRACE_LOCK_REQUESTING_FOR("requesting for"),
TRACE_LOCK_TIMEOUT_AFTER("timeout after "),
TRACE_LOCK_UNLOCK("unlock"),
TRACE_LOCK_ADDED_FOR("added for"),
TRACE_LOCK_ADD_UPGRADED_FOR("add (upgraded) for ");
private final String eventText;
TraceLockEvent(String eventText) {
this.eventText = eventText;
}
public String getEventText() {
return eventText;
}
}
private static final String NO_EXTRA_INFO = "";
static { static {
if (SysProperties.THREAD_DEADLOCK_DETECTOR) { if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
WAITING_FOR_LOCK = new DebuggingThreadLocal<>(); WAITING_FOR_LOCK = new DebuggingThreadLocal<>();
...@@ -192,7 +216,7 @@ public class MVTable extends TableBase { ...@@ -192,7 +216,7 @@ public class MVTable extends TableBase {
} }
private void doLock1(Session session, int lockMode, boolean exclusive) { private void doLock1(Session session, int lockMode, boolean exclusive) {
traceLock(session, exclusive, "requesting for"); traceLock(session, exclusive, TraceLockEvent.TRACE_LOCK_REQUESTING_FOR, NO_EXTRA_INFO);
// don't get the current time unless necessary // don't get the current time unless necessary
long max = 0; long max = 0;
boolean checkDeadlock = false; boolean checkDeadlock = false;
...@@ -219,11 +243,11 @@ public class MVTable extends TableBase { ...@@ -219,11 +243,11 @@ public class MVTable extends TableBase {
max = now + TimeUnit.MILLISECONDS.toNanos(session.getLockTimeout()); max = now + TimeUnit.MILLISECONDS.toNanos(session.getLockTimeout());
} else if (now >= max) { } else if (now >= max) {
traceLock(session, exclusive, traceLock(session, exclusive,
"timeout after " + session.getLockTimeout()); TraceLockEvent.TRACE_LOCK_TIMEOUT_AFTER, NO_EXTRA_INFO+session.getLockTimeout());
throw DbException.get(ErrorCode.LOCK_TIMEOUT_1, getName()); throw DbException.get(ErrorCode.LOCK_TIMEOUT_1, getName());
} }
try { try {
traceLock(session, exclusive, "waiting for"); traceLock(session, exclusive, TraceLockEvent.TRACE_LOCK_WAITING_FOR, NO_EXTRA_INFO);
if (database.getLockMode() == Constants.LOCK_MODE_TABLE_GC) { if (database.getLockMode() == Constants.LOCK_MODE_TABLE_GC) {
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
long free = Runtime.getRuntime().freeMemory(); long free = Runtime.getRuntime().freeMemory();
...@@ -251,7 +275,7 @@ public class MVTable extends TableBase { ...@@ -251,7 +275,7 @@ public class MVTable extends TableBase {
if (exclusive) { if (exclusive) {
if (lockExclusiveSession == null) { if (lockExclusiveSession == null) {
if (lockSharedSessions.isEmpty()) { if (lockSharedSessions.isEmpty()) {
traceLock(session, exclusive, "added for"); traceLock(session, exclusive, TraceLockEvent.TRACE_LOCK_ADDED_FOR, NO_EXTRA_INFO);
session.addLock(this); session.addLock(this);
lockExclusiveSession = session; lockExclusiveSession = session;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) { if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
...@@ -263,7 +287,7 @@ public class MVTable extends TableBase { ...@@ -263,7 +287,7 @@ public class MVTable extends TableBase {
return true; return true;
} else if (lockSharedSessions.size() == 1 && } else if (lockSharedSessions.size() == 1 &&
lockSharedSessions.containsKey(session)) { lockSharedSessions.containsKey(session)) {
traceLock(session, exclusive, "add (upgraded) for "); traceLock(session, exclusive, TraceLockEvent.TRACE_LOCK_ADD_UPGRADED_FOR, NO_EXTRA_INFO);
lockExclusiveSession = session; lockExclusiveSession = session;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) { if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (EXCLUSIVE_LOCKS.get() == null) { if (EXCLUSIVE_LOCKS.get() == null) {
...@@ -289,7 +313,7 @@ public class MVTable extends TableBase { ...@@ -289,7 +313,7 @@ public class MVTable extends TableBase {
} }
} }
if (!lockSharedSessions.containsKey(session)) { if (!lockSharedSessions.containsKey(session)) {
traceLock(session, exclusive, "ok"); traceLock(session, exclusive, TraceLockEvent.TRACE_LOCK_OK, NO_EXTRA_INFO);
session.addLock(this); session.addLock(this);
lockSharedSessions.put(session, session); lockSharedSessions.put(session, session);
if (SysProperties.THREAD_DEADLOCK_DETECTOR) { if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
...@@ -387,10 +411,10 @@ public class MVTable extends TableBase { ...@@ -387,10 +411,10 @@ public class MVTable extends TableBase {
} }
} }
private void traceLock(Session session, boolean exclusive, String s) { private void traceLock(Session session, boolean exclusive, TraceLockEvent eventEnum, String extraInfo) {
if (traceLock.isDebugEnabled()) { if (traceLock.isDebugEnabled()) {
traceLock.debug("{0} {1} {2} {3}", session.getId(), traceLock.debug("{0} {1} {2} {3}", session.getId(),
exclusive ? "exclusive write lock" : "shared read lock", s, exclusive ? "exclusive write lock" : "shared read lock", eventEnum.getEventText(),
getName()); getName());
} }
} }
...@@ -404,11 +428,11 @@ public class MVTable extends TableBase { ...@@ -404,11 +428,11 @@ public class MVTable extends TableBase {
public boolean isLockedExclusivelyBy(Session session) { public boolean isLockedExclusivelyBy(Session session) {
return lockExclusiveSession == session; return lockExclusiveSession == session;
} }
@Override @Override
public void unlock(Session s) { public void unlock(Session s) {
if (database != null) { if (database != null) {
traceLock(s, lockExclusiveSession == s, "unlock"); traceLock(s, lockExclusiveSession == s, TraceLockEvent.TRACE_LOCK_UNLOCK, NO_EXTRA_INFO);
if (lockExclusiveSession == s) { if (lockExclusiveSession == s) {
lockExclusiveSession = null; lockExclusiveSession = null;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) { if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
......
...@@ -86,6 +86,7 @@ public abstract class Table extends SchemaObjectBase { ...@@ -86,6 +86,7 @@ public abstract class Table extends SchemaObjectBase {
private boolean checkForeignKeyConstraints = true; private boolean checkForeignKeyConstraints = true;
private boolean onCommitDrop, onCommitTruncate; private boolean onCommitDrop, onCommitTruncate;
private volatile Row nullRow; private volatile Row nullRow;
private boolean tableExpression;
public Table(Schema schema, int id, String name, boolean persistIndexes, public Table(Schema schema, int id, String name, boolean persistIndexes,
boolean persistData) { boolean persistData) {
...@@ -195,7 +196,6 @@ public abstract class Table extends SchemaObjectBase { ...@@ -195,7 +196,6 @@ public abstract class Table extends SchemaObjectBase {
* @param operation the operation * @param operation the operation
* @param row the row * @param row the row
*/ */
@SuppressWarnings("unused")
public void commit(short operation, Row row) { public void commit(short operation, Row row) {
// nothing to do // nothing to do
} }
...@@ -233,7 +233,6 @@ public abstract class Table extends SchemaObjectBase { ...@@ -233,7 +233,6 @@ public abstract class Table extends SchemaObjectBase {
* @param allColumnsSet all columns * @param allColumnsSet all columns
* @return the scan index * @return the scan index
*/ */
@SuppressWarnings("unused")
public Index getScanIndex(Session session, int[] masks, public Index getScanIndex(Session session, int[] masks,
TableFilter[] filters, int filter, SortOrder sortOrder, TableFilter[] filters, int filter, SortOrder sortOrder,
HashSet<Column> allColumnsSet) { HashSet<Column> allColumnsSet) {
...@@ -465,7 +464,6 @@ public abstract class Table extends SchemaObjectBase { ...@@ -465,7 +464,6 @@ public abstract class Table extends SchemaObjectBase {
* @param session the session * @param session the session
* @return true if it is * @return true if it is
*/ */
@SuppressWarnings("unused")
public boolean isLockedExclusivelyBy(Session session) { public boolean isLockedExclusivelyBy(Session session) {
return false; return false;
} }
...@@ -836,7 +834,7 @@ public abstract class Table extends SchemaObjectBase { ...@@ -836,7 +834,7 @@ public abstract class Table extends SchemaObjectBase {
} }
/** /**
* Remove the given view from the list. * Remove the given view from the dependent views list.
* *
* @param view the view to remove * @param view the view to remove
*/ */
...@@ -1166,7 +1164,6 @@ public abstract class Table extends SchemaObjectBase { ...@@ -1166,7 +1164,6 @@ public abstract class Table extends SchemaObjectBase {
* @return an object array with the sessions involved in the deadlock, or * @return an object array with the sessions involved in the deadlock, or
* null * null
*/ */
@SuppressWarnings("unused")
public ArrayList<Session> checkDeadlock(Session session, Session clash, public ArrayList<Session> checkDeadlock(Session session, Session clash,
Set<Session> visited) { Set<Session> visited) {
return null; return null;
...@@ -1242,5 +1239,12 @@ public abstract class Table extends SchemaObjectBase { ...@@ -1242,5 +1239,12 @@ public abstract class Table extends SchemaObjectBase {
public boolean isMVStore() { public boolean isMVStore() {
return false; return false;
} }
public void setTableExpression(boolean tableExpression) {
this.tableExpression = tableExpression;
}
public boolean isTableExpression() {
return tableExpression;
}
} }
...@@ -54,6 +54,7 @@ import org.h2.test.db.TestOpenClose; ...@@ -54,6 +54,7 @@ import org.h2.test.db.TestOpenClose;
import org.h2.test.db.TestOptimizations; import org.h2.test.db.TestOptimizations;
import org.h2.test.db.TestOptimizerHints; import org.h2.test.db.TestOptimizerHints;
import org.h2.test.db.TestOutOfMemory; import org.h2.test.db.TestOutOfMemory;
import org.h2.test.db.TestPersistentCommonTableExpressions;
import org.h2.test.db.TestPowerOff; import org.h2.test.db.TestPowerOff;
import org.h2.test.db.TestQueryCache; import org.h2.test.db.TestQueryCache;
import org.h2.test.db.TestReadOnly; import org.h2.test.db.TestReadOnly;
...@@ -762,6 +763,10 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1` ...@@ -762,6 +763,10 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
addTest(new TestReadOnly()); addTest(new TestReadOnly());
addTest(new TestRecursiveQueries()); addTest(new TestRecursiveQueries());
addTest(new TestGeneralCommonTableQueries()); addTest(new TestGeneralCommonTableQueries());
if (!memory) {
// requires persistent store for reconnection tests
addTest(new TestPersistentCommonTableExpressions());
}
addTest(new TestRights()); addTest(new TestRights());
addTest(new TestRunscript()); addTest(new TestRunscript());
addTest(new TestSQLInjection()); addTest(new TestSQLInjection());
......
...@@ -124,7 +124,6 @@ public abstract class TestBase { ...@@ -124,7 +124,6 @@ public abstract class TestBase {
* *
* @param seed the random seed value * @param seed the random seed value
*/ */
@SuppressWarnings("unused")
public void testCase(int seed) throws Exception { public void testCase(int seed) throws Exception {
// do nothing // do nothing
} }
......
package org.h2.test.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.h2.test.TestBase;
/**
* Base class for common table expression tests
*/
public abstract class AbstractBaseForCommonTableExpressions extends TestBase {
protected void testRepeatedQueryWithSetup(int maxRetries, String[] expectedRowData, String[] expectedColumnNames, int expectedNumbeOfRows, String setupSQL,
String withQuery, int closeAndReopenDatabaseConnectionOnIteration, String[] expectedColumnTypes) throws SQLException {
deleteDb("commonTableExpressionQueries");
Connection conn = getConnection("commonTableExpressionQueries");
PreparedStatement prep;
ResultSet rs;
for (int queryRunTries = 1; queryRunTries <= maxRetries; queryRunTries++) {
Statement stat = conn.createStatement();
stat.execute(setupSQL);
stat.close();
// close and re-open connection for one iteration to make sure the query work between connections
if (queryRunTries == closeAndReopenDatabaseConnectionOnIteration) {
conn.close();
conn = getConnection("commonTableExpressionQueries");
}
prep = conn.prepareStatement(withQuery);
rs = prep.executeQuery();
for (int columnIndex = 1; columnIndex <= rs.getMetaData().getColumnCount(); columnIndex++) {
assertTrue(rs.getMetaData().getColumnLabel(columnIndex) != null);
assertEquals(expectedColumnNames[columnIndex - 1], rs.getMetaData().getColumnLabel(columnIndex));
assertEquals("wrongly type column "+rs.getMetaData().getColumnLabel(columnIndex)+" on iteration#"+queryRunTries,
expectedColumnTypes[columnIndex - 1], rs.getMetaData().getColumnTypeName(columnIndex));
}
int rowNdx = 0;
while (rs.next()) {
StringBuffer buf = new StringBuffer();
for (int columnIndex = 1; columnIndex <= rs.getMetaData().getColumnCount(); columnIndex++) {
buf.append("|"+rs.getString(columnIndex));
}
assertEquals(expectedRowData[rowNdx], buf.toString());
rowNdx++;
}
assertEquals(expectedNumbeOfRows, rowNdx);
rs.close();
prep.close();
}
conn.close();
deleteDb("commonTableExpressionQueries");
}
}
...@@ -20,6 +20,10 @@ import org.h2.util.IOUtils; ...@@ -20,6 +20,10 @@ import org.h2.util.IOUtils;
*/ */
public class TestMvccMultiThreaded2 extends TestBase { public class TestMvccMultiThreaded2 extends TestBase {
private static final int TEST_THREAD_COUNT = 100;
private static final int TEST_TIME_SECONDS = 60;
private static final boolean DISPLAY_STATS = false;
private static final String URL = ";MVCC=TRUE;LOCK_TIMEOUT=120000;MULTI_THREADED=TRUE"; private static final String URL = ";MVCC=TRUE;LOCK_TIMEOUT=120000;MULTI_THREADED=TRUE";
/** /**
...@@ -62,21 +66,49 @@ public class TestMvccMultiThreaded2 extends TestBase { ...@@ -62,21 +66,49 @@ public class TestMvccMultiThreaded2 extends TestBase {
conn.commit(); conn.commit();
ArrayList<SelectForUpdate> threads = new ArrayList<>(); ArrayList<SelectForUpdate> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) { for (int i = 0; i < TEST_THREAD_COUNT; i++) {
SelectForUpdate sfu = new SelectForUpdate(); SelectForUpdate sfu = new SelectForUpdate();
sfu.setName("Test SelectForUpdate Thread#"+i);
threads.add(sfu); threads.add(sfu);
sfu.start(); sfu.start();
} }
// give any of the 100 threads a chance to start by yielding the processor to them
Thread.yield();
// gather stats on threads after they finished
@SuppressWarnings("unused")
int minProcessed = Integer.MAX_VALUE, maxProcessed = 0, totalProcessed = 0;
for (SelectForUpdate sfu : threads) { for (SelectForUpdate sfu : threads) {
// make sure all threads have stopped by joining with them
sfu.join(); sfu.join();
totalProcessed += sfu.iterationsProcessed;
if (sfu.iterationsProcessed > maxProcessed) {
maxProcessed = sfu.iterationsProcessed;
}
if (sfu.iterationsProcessed < minProcessed) {
minProcessed = sfu.iterationsProcessed;
}
}
if (DISPLAY_STATS) {
System.out.println(String.format("+ INFO: TestMvccMultiThreaded2 RUN STATS threads=%d, minProcessed=%d, maxProcessed=%d, "+
"totalProcessed=%d, averagePerThread=%d, averagePerThreadPerSecond=%d\n",
TEST_THREAD_COUNT, minProcessed, maxProcessed, totalProcessed, totalProcessed/TEST_THREAD_COUNT,
totalProcessed/(TEST_THREAD_COUNT * TEST_TIME_SECONDS)));
} }
IOUtils.closeSilently(conn); IOUtils.closeSilently(conn);
deleteDb(getTestName()); deleteDb(getTestName());
} }
/**
* Worker test thread selecting for update
*/
private class SelectForUpdate extends Thread { private class SelectForUpdate extends Thread {
public int iterationsProcessed;
@Override @Override
public void run() { public void run() {
...@@ -86,6 +118,10 @@ public class TestMvccMultiThreaded2 extends TestBase { ...@@ -86,6 +118,10 @@ public class TestMvccMultiThreaded2 extends TestBase {
try { try {
conn = getConnection(getTestName() + URL); conn = getConnection(getTestName() + URL);
conn.setAutoCommit(false); conn.setAutoCommit(false);
// give the other threads a chance to start up before going into our work loop
Thread.yield();
while (!done) { while (!done) {
try { try {
PreparedStatement ps = conn.prepareStatement( PreparedStatement ps = conn.prepareStatement(
...@@ -97,17 +133,22 @@ public class TestMvccMultiThreaded2 extends TestBase { ...@@ -97,17 +133,22 @@ public class TestMvccMultiThreaded2 extends TestBase {
assertTrue(rs.getInt(2) == 100); assertTrue(rs.getInt(2) == 100);
conn.commit(); conn.commit();
iterationsProcessed++;
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - start > 1000 * 60) if (now - start > 1000 * TEST_TIME_SECONDS) {
done = true; done = true;
}
} catch (JdbcSQLException e1) { } catch (JdbcSQLException e1) {
throw e1; throw e1;
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
TestBase.logError("error", e); TestBase.logError("SQL error from thread "+getName(), e);
} } catch (Exception e) {
TestBase.logError("General error from thread "+getName(), e);
throw e;
}
IOUtils.closeSilently(conn); IOUtils.closeSilently(conn);
} }
} }
......
...@@ -133,6 +133,9 @@ public class TestScript extends TestBase { ...@@ -133,6 +133,9 @@ public class TestScript extends TestBase {
"parsedatetime", "quarter", "second", "week", "year" }) { "parsedatetime", "quarter", "second", "week", "year" }) {
testScript("functions/timeanddate/" + s + ".sql"); testScript("functions/timeanddate/" + s + ".sql");
} }
for (String s : new String[] { "with", "mergeUsing" }) {
testScript("dml/" + s + ".sql");
}
deleteDb("script"); deleteDb("script");
System.out.flush(); System.out.flush();
} }
......
-- Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0,
-- and the EPL 1.0 (http://h2database.com/html/license.html).
-- Initial Developer: H2 Group
--
CREATE TABLE PARENT(ID INT, NAME VARCHAR, PRIMARY KEY(ID) );
> ok
MERGE INTO PARENT AS P
USING (SELECT X AS ID, 'Coco'||X AS NAME FROM SYSTEM_RANGE(1,2) ) AS S
ON (P.ID = S.ID AND 1=1 AND S.ID = P.ID)
WHEN MATCHED THEN
UPDATE SET P.NAME = S.NAME WHERE 2 = 2 WHEN NOT
MATCHED THEN
INSERT (ID, NAME) VALUES (S.ID, S.NAME);
> update count: 2
SELECT * FROM PARENT;
> ID NAME
> -- -----
> 1 Coco1
> 2 Coco2
EXPLAIN PLAN
MERGE INTO PARENT AS P
USING (SELECT X AS ID, 'Coco'||X AS NAME FROM SYSTEM_RANGE(1,2) ) AS S
ON (P.ID = S.ID AND 1=1 AND S.ID = P.ID)
WHEN MATCHED THEN
UPDATE SET P.NAME = S.NAME WHERE 2 = 2 WHEN NOT
MATCHED THEN
INSERT (ID, NAME) VALUES (S.ID, S.NAME);
> PLAN
> ---------------------------------------------------------------------------------------------------------------------------------
> MERGE INTO PUBLIC.PARENT(ID, NAME) KEY(ID) SELECT X AS ID, ('Coco' || X) AS NAME FROM SYSTEM_RANGE(1, 2) /* PUBLIC.RANGE_INDEX */
\ No newline at end of file
-- Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0,
-- and the EPL 1.0 (http://h2database.com/html/license.html).
-- Initial Developer: H2 Group
--
explain with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r;
> PLAN
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> WITH RECURSIVE R(N) AS ( (SELECT 1 FROM SYSTEM_RANGE(1, 1) /* PUBLIC.RANGE_INDEX */) UNION ALL (SELECT (N + 1) FROM PUBLIC.R /* PUBLIC.R.tableScan */ WHERE N < 3) ) SELECT N FROM R R /* null */
> rows: 1
select sum(n) from (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
);
> SUM(N)
> ------
> 6
> rows: 1
select sum(n) from (select 0) join (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
) on 1=1;
> SUM(N)
> ------
> 6
> rows: 1
select 0 from (
select 0 where 0 in (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
)
);
> 0
> -
> rows: 0
with
r0(n,k) as (select -1, 0),
r1(n,k) as ((select 1, 0) union all (select n+1,k+1 from r1 where n <= 3)),
r2(n,k) as ((select 10,0) union all (select n+1,k+1 from r2 where n <= 13))
select r1.k, r0.n as N0, r1.n AS N1, r2.n AS n2 from r0 inner join r1 ON r1.k= r0.k inner join r2 ON r1.k= r2.k;
> K N0 N1 N2
> - -- -- --
> 0 -1 1 10
> rows: 1
\ No newline at end of file
...@@ -9369,49 +9369,6 @@ select 0 from (( ...@@ -9369,49 +9369,6 @@ select 0 from ((
}; };
> update count: 0 > update count: 0
explain with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r;
> PLAN
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> WITH RECURSIVE R(N) AS ( (SELECT 1 FROM SYSTEM_RANGE(1, 1) /* PUBLIC.RANGE_INDEX */) UNION ALL (SELECT (N + 1) FROM PUBLIC.R /* PUBLIC.R.tableScan */ WHERE N < 3) ) SELECT N FROM R R /* null */
> rows: 1
select sum(n) from (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
);
> SUM(N)
> ------
> 6
> rows: 1
select sum(n) from (select 0) join (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
) on 1=1;
> SUM(N)
> ------
> 6
> rows: 1
select 0 from (
select 0 where 0 in (
with recursive r(n) as (
(select 1) union all (select n+1 from r where n < 3)
)
select n from r
)
);
> 0
> -
> rows: 0
create table x(id int not null); create table x(id int not null);
> ok > ok
......
...@@ -51,11 +51,11 @@ public class TestMathUtils extends TestBase { ...@@ -51,11 +51,11 @@ public class TestMathUtils extends TestBase {
private void testNextPowerOf2Int() { private void testNextPowerOf2Int() {
// the largest power of two that fits into an integer // the largest power of two that fits into an integer
final int LARGEST_POW2 = 0x40000000; final int largestPower2 = 0x40000000;
int[] testValues = { 0, 1, 2, 3, 4, 12, 17, 500, 1023, int[] testValues = { 0, 1, 2, 3, 4, 12, 17, 500, 1023,
LARGEST_POW2-500, LARGEST_POW2 }; largestPower2 - 500, largestPower2 };
int[] resultValues = { 1, 1, 2, 4, 4, 16, 32, 512, 1024, int[] resultValues = { 1, 1, 2, 4, 4, 16, 32, 512, 1024,
LARGEST_POW2, LARGEST_POW2 }; largestPower2, largestPower2 };
for (int i = 0; i < testValues.length; i++) { for (int i = 0; i < testValues.length; i++) {
assertEquals(resultValues[i], MathUtils.nextPowerOf2(testValues[i])); assertEquals(resultValues[i], MathUtils.nextPowerOf2(testValues[i]));
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论