提交 d5e100f4 authored 作者: Owner's avatar Owner

Pre master merge commit

上级 b1359724
......@@ -14,3 +14,4 @@ test.out.txt
*.log
target/
src/main/org/h2/res/help.csv
_tmp*
......@@ -5149,8 +5149,11 @@ public class Parser {
if(isToken("SELECT")) {
Query query = parseSelectUnion();
query.setPrepareAlways(!isPersistent);
query.setPrepareAlways(true);
query.setNeverLazy(true);
if(isPersistent){
query.session = session.getDatabase().getSystemSession();
}
p = query;
}
else if(readIf("INSERT")) {
......@@ -5199,7 +5202,7 @@ public class Parser {
Table recursiveTable=null;
ArrayList<Column> columns = New.arrayList();
String[] cols = null;
Database db = session.getDatabase();
Database db = targetSession.getDatabase();
// column names are now optional - they can be inferred from the named
// query, if not supplied by user
......@@ -5214,7 +5217,7 @@ public class Parser {
Table oldViewFound = null;
if(isPersistent){
oldViewFound = getSchema().findTableOrView(session, cteViewName);
oldViewFound = getSchema().findTableOrView(targetSession, cteViewName);
}
else{
oldViewFound = targetSession.findLocalTempTable(cteViewName);
......@@ -5230,8 +5233,8 @@ public class Parser {
cteViewName);
}
if(isPersistent){
oldViewFound.lock(session, true, true);
session.getDatabase().removeSchemaObject(session, oldViewFound);
oldViewFound.lock(targetSession, true, true);
targetSession.getDatabase().removeSchemaObject(targetSession, oldViewFound);
}else{
targetSession.removeLocalTempTable(oldViewFound);
......@@ -5256,10 +5259,10 @@ public class Parser {
// this gets a meta table lock that is not released
recursiveTable = schema.createTable(recursiveTableData);
if(isPersistent){
// this unlock is to prevent beed from schema.createTable()
database.unlockMeta(targetSession);
// this unlock is to prevent lock leak from schema.createTable()
db.unlockMeta(targetSession);
synchronized (targetSession) {
db.addSchemaObject(session, recursiveTable);
db.addSchemaObject(targetSession, recursiveTable);
}
}else{
targetSession.addLocalTempTable(recursiveTable);
......@@ -5271,14 +5274,17 @@ public class Parser {
read("AS");
read("(");
Query withQuery = parseSelect();
if(isPersistent){
withQuery.session = targetSession;
}
read(")");
columnTemplateList = createQueryColumnTemplateList(cols, withQuery, querySQLOutput);
} finally {
if(recursiveTable!=null){
if(isPersistent){
recursiveTable.lock(session, true, true);
session.getDatabase().removeSchemaObject(session, recursiveTable);
recursiveTable.lock(targetSession, true, true);
targetSession.getDatabase().removeSchemaObject(targetSession, recursiveTable);
}else{
targetSession.removeLocalTempTable(recursiveTable);
......@@ -5312,7 +5318,7 @@ public class Parser {
Query theQuery, String[] querySQLOutput) {
List<Column> columnTemplateList = new ArrayList<>();
theQuery.prepare();
// array of length 1 is to receive extra 'output' field in addition to
// String array of length 1 is to receive extra 'output' field in addition to
// return value
querySQLOutput[0] = StringUtils.cache(theQuery.getPlanSQL());
ColumnNamer columnNamer = new ColumnNamer(theQuery.getSession());
......@@ -5332,10 +5338,10 @@ public class Parser {
private TableView createCTEView(String cteViewName, String querySQL,
List<Column> columnTemplateList, boolean allowRecursiveQueryDetection,
boolean addViewToSession, boolean isPersistent) {
Session targetSession = /*isPersistent ? database.getSystemSession() :*/session;
Database db = session.getDatabase();
Session targetSession = isPersistent ? database.getSystemSession() : session;
Database db = targetSession.getDatabase();
Schema schema = getSchemaWithDefault();
int id = database.allocateObjectId();
int id = db.allocateObjectId();
Column[] columnTemplateArray = columnTemplateList.toArray(new Column[0]);
// No easy way to determine if this is a recursive query up front, so we just compile
// it twice - once without the flag set, and if we didn't see a recursive term,
......@@ -5347,9 +5353,9 @@ public class Parser {
allowRecursiveQueryDetection, false /* literalsChecked */);
if (!view.isRecursiveQueryDetected() && allowRecursiveQueryDetection) {
if(isPersistent){
db.addSchemaObject(session, view);
view.lock(session, true, true);
session.getDatabase().removeSchemaObject(session, view);
db.addSchemaObject(targetSession, view);
view.lock(targetSession, true, true);
targetSession.getDatabase().removeSchemaObject(targetSession, view);
}else{
session.removeLocalTempTable(view);
}
......@@ -5364,7 +5370,9 @@ public class Parser {
view.setOnCommitDrop(false);
if(addViewToSession){
if(isPersistent){
db.addSchemaObject(session, view);
db.addSchemaObject(targetSession, view);
view.unlock(targetSession);
db.unlockMeta(targetSession);
}
else{
targetSession.addLocalTempTable(view);
......
......@@ -23,6 +23,7 @@ import org.h2.schema.Sequence;
import org.h2.table.Column;
import org.h2.table.IndexColumn;
import org.h2.table.Table;
import org.h2.util.ColumnNamer;
import org.h2.util.New;
import org.h2.value.DataType;
import org.h2.value.Value;
......@@ -230,10 +231,11 @@ public class CreateTable extends SchemaCommand {
private void generateColumnsFromQuery() {
int columnCount = asQuery.getColumnCount();
ArrayList<Expression> expressions = asQuery.getExpressions();
ColumnNamer columnNamer= new ColumnNamer(session);
for (int i = 0; i < columnCount; i++) {
Expression expr = expressions.get(i);
int type = expr.getType();
String name = expr.getAlias();
String name = columnNamer.getColumnName(expr,i,expr.getAlias());
long precision = expr.getPrecision();
int displaySize = expr.getDisplaySize();
DataType dt = DataType.getDataType(type);
......
......@@ -101,6 +101,7 @@ public class DropTable extends SchemaCommand {
Database db = session.getDatabase();
db.lockMeta(session);
db.removeSchemaObject(session, table);
session.getDatabase().flushDeferredRemoveSchemaObject();
}
if (next != null) {
next.executeDrop();
......
......@@ -68,8 +68,12 @@ public class DropView extends SchemaCommand {
}
}
// TODO: Where is the ConstraintReferential.CASCADE style drop processing ? It's
// supported from imported keys - but not for dependant
view.lock(session, true, true);
session.getDatabase().removeSchemaObject(session, view);
session.getDatabase().flushDeferredRemoveSchemaObject();
}
return 0;
}
......
......@@ -1087,8 +1087,13 @@ public class Select extends Query {
for (TableFilter f : topFilters) {
Table t = f.getTable();
boolean isPersistent = !t.isTemporary();
System.out.println("topFilters[]="+t.getName());
if (t.isView() && ((TableView) t).isRecursive()) {
buff.append("WITH RECURSIVE ");
TableView tv = ((TableView) t);
buff.append("WITH ");
if(tv.isRecursive()){
buff.append("RECURSIVE ");
}
if(isPersistent){
buff.append("PERSISTENT ");
}
......@@ -1099,7 +1104,15 @@ public class Select extends Query {
buff.append(c.getName());
}
String theSQL = t.getSQL();
System.out.println("getPlanSQL.theSQL="+theSQL);
System.out.println("getPlanSQL.sqlStatement="+sqlStatement);
if(!sqlStatement.contains("?") && sqlStatement.toUpperCase().contains("SELECT") && session.isParsingView()){
theSQL = extractNamedCTEQueryFromSQL(t.getName(),sqlStatement);
if(!(theSQL.startsWith("(")&&theSQL.endsWith(")"))){
theSQL = "( "+theSQL+" )";
}
}
else if(!(theSQL.startsWith("(")&&theSQL.endsWith(")"))){
StatementBuilder buffSelect = new StatementBuilder();
buffSelect.append("( SELECT ");
buffSelect.resetCount();
......@@ -1107,7 +1120,7 @@ public class Select extends Query {
buffSelect.appendExceptFirst(",");
buffSelect.append(c.getName());
}
buffSelect.append(" FROM "+t.getSQL()+") ");
buffSelect.append(" FROM "+t.getSQL()+" ) ");
theSQL = buffSelect.toString();
}
buff.append(") AS ").append(theSQL).append("\n");
......@@ -1223,6 +1236,13 @@ public class Select extends Query {
return buff.toString();
}
private String extractNamedCTEQueryFromSQL(String viewName, String sqlStatement) {
Table existingTableOrView = session.getDatabase().getSchema(session.getCurrentSchemaName()).findTableOrView(session, viewName);
TableView existingView = (TableView) existingTableOrView;
System.out.println("existingView.getSQL()="+existingView.getSQL());
return existingView.getSQL();
}
public void setHaving(Expression having) {
this.having = having;
}
......
......@@ -11,6 +11,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
......@@ -202,6 +204,7 @@ public class Database implements DataHandler {
private int queryStatisticsMaxEntries = Constants.QUERY_STATISTICS_MAX_ENTRIES;
private QueryStatisticsData queryStatisticsData;
private RowFactory rowFactory = RowFactory.DEFAULT;
private ConcurrentHashMap<SchemaObject, Session> removeSchemaObjectQueue = new ConcurrentHashMap<>();
public Database(ConnectionInfo ci, String cipher) {
String name = ci.getName();
......@@ -917,6 +920,27 @@ public class Database implements DataHandler {
return wasLocked;
}
/**
* Lock the metadata table for updates - but don't wait if it's already locked...
*
* @param session the session
* @return whether it was already locked before by this session - or null if locked by other session
*/
public Boolean lockMetaNoWait(Session session) {
// this method can not be synchronized on the database object,
// as unlocking is also synchronized on the database object -
// so if locking starts just before unlocking, locking could
// never be successful
if (meta == null) {
return true;
}
if(meta.isLockedExclusively() && ! meta.isLockedExclusivelyBy(session)){
return null;
}
boolean wasLocked = meta.lock(session, true, true);
return wasLocked;
}
/**
* Unlock the metadata table.
*
......@@ -1864,7 +1888,12 @@ public class Database implements DataHandler {
}
}
checkWritingAllowed();
lockMeta(session);
Boolean wasLocked = lockMetaNoWait(session);
if(wasLocked==null){
removeSchemaObjectQueue.put(obj,session);
System.out.println("deferred removal scheduled="+obj.getName()+",wasLocked="+wasLocked);
return;
}
synchronized (this) {
Comment comment = findComment(obj);
if (comment != null) {
......@@ -1881,7 +1910,24 @@ public class Database implements DataHandler {
}
obj.removeChildrenAndResources(session);
}
System.out.println("Removing meta lock");
removeMeta(session, id);
flushDeferredRemoveSchemaObject();
}
}
public void flushDeferredRemoveSchemaObject() {
Iterator<Entry<SchemaObject, Session>> i = removeSchemaObjectQueue.entrySet().iterator();
while(i.hasNext()){
Entry<SchemaObject, Session> pair = i.next();
i.remove();
System.out.println("re-attempting deferred removal="+pair.getKey().getName()+",size="+removeSchemaObjectQueue.size());
removeSchemaObject(pair.getValue(),pair.getKey());
if(!removeSchemaObjectQueue.contains(pair.getKey())){
System.out.println("completed deferred removal="+pair.getKey().getName()+",size="+removeSchemaObjectQueue.size());
}
}
}
......
......@@ -49,6 +49,14 @@ import org.h2.value.Value;
*/
public class MVTable extends TableBase {
private static final String TRACE_LOCK_OK = "ok";
private static final String TRACE_LOCK_WAITING_FOR = "waiting for";
private static final String TRACE_LOCK_REQUESTING_FOR = "requesting for";
private static final String TRACE_LOCK_TIMEOUT_AFTER = "timeout after ";
private static final String TRACE_LOCK_UNLOCK = "unlock";
private static final String TRACE_LOCK_ADDED_FOR = "added for";
private static final String TRACE_LOCK_ADD_UPGRADED_FOR = "add (upgraded) for ";
/**
* The table name this thread is waiting to lock.
*/
......@@ -80,6 +88,7 @@ public class MVTable extends TableBase {
private final ArrayList<Index> indexes = New.arrayList();
private volatile long lastModificationId;
private volatile Session lockExclusiveSession;
private volatile Throwable lockExclusiveSessionStackTrace;
// using a ConcurrentHashMap as a set
private final ConcurrentHashMap<Session, Session> lockSharedSessions =
......@@ -192,7 +201,7 @@ public class MVTable extends TableBase {
}
private void doLock1(Session session, int lockMode, boolean exclusive) {
traceLock(session, exclusive, "requesting for");
traceLock(session, exclusive, TRACE_LOCK_REQUESTING_FOR);
// don't get the current time unless necessary
long max = 0;
boolean checkDeadlock = false;
......@@ -219,11 +228,11 @@ public class MVTable extends TableBase {
max = now + TimeUnit.MILLISECONDS.toNanos(session.getLockTimeout());
} else if (now >= max) {
traceLock(session, exclusive,
"timeout after " + session.getLockTimeout());
TRACE_LOCK_TIMEOUT_AFTER + session.getLockTimeout());
throw DbException.get(ErrorCode.LOCK_TIMEOUT_1, getName());
}
try {
traceLock(session, exclusive, "waiting for");
traceLock(session, exclusive, TRACE_LOCK_WAITING_FOR);
if (database.getLockMode() == Constants.LOCK_MODE_TABLE_GC) {
for (int i = 0; i < 20; i++) {
long free = Runtime.getRuntime().freeMemory();
......@@ -251,7 +260,7 @@ public class MVTable extends TableBase {
if (exclusive) {
if (lockExclusiveSession == null) {
if (lockSharedSessions.isEmpty()) {
traceLock(session, exclusive, "added for");
traceLock(session, exclusive, TRACE_LOCK_ADDED_FOR);
session.addLock(this);
lockExclusiveSession = session;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
......@@ -263,7 +272,7 @@ public class MVTable extends TableBase {
return true;
} else if (lockSharedSessions.size() == 1 &&
lockSharedSessions.containsKey(session)) {
traceLock(session, exclusive, "add (upgraded) for ");
traceLock(session, exclusive, TRACE_LOCK_ADD_UPGRADED_FOR);
lockExclusiveSession = session;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (EXCLUSIVE_LOCKS.get() == null) {
......@@ -289,7 +298,7 @@ public class MVTable extends TableBase {
}
}
if (!lockSharedSessions.containsKey(session)) {
traceLock(session, exclusive, "ok");
traceLock(session, exclusive, TRACE_LOCK_OK);
session.addLock(this);
lockSharedSessions.put(session, session);
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
......@@ -387,11 +396,26 @@ public class MVTable extends TableBase {
}
}
private void traceLock(Session session, boolean exclusive, String s) {
private void traceLock(Session session, boolean exclusive, String statusText) {
if (traceLock.isDebugEnabled()) {
traceLock.debug("{0} {1} {2} {3}", session.getId(),
exclusive ? "exclusive write lock" : "shared read lock", s,
exclusive ? "exclusive write lock" : "shared read lock", statusText,
getName());
// create a stack trace when the lock is granted so we can debug where that was...
if(statusText.equals(TRACE_LOCK_ADDED_FOR) || statusText.equals(TRACE_LOCK_ADD_UPGRADED_FOR)){
lockExclusiveSessionStackTrace = new Throwable("trace lock - lock granted stack trace");
}
// clear the stack trace of the granted lock, on unlock
if(statusText.equals(TRACE_LOCK_UNLOCK)){
lockExclusiveSessionStackTrace = null;
}
// show the stack trace where the lock was granted, if a timeout happens...
if(statusText.contains(TRACE_LOCK_TIMEOUT_AFTER) && lockExclusiveSessionStackTrace!=null){
lockExclusiveSessionStackTrace.printStackTrace();
}
}
}
......@@ -402,13 +426,23 @@ public class MVTable extends TableBase {
@Override
public boolean isLockedExclusivelyBy(Session session) {
return lockExclusiveSession == session;
Session localSession = lockExclusiveSession;
if(localSession!=null){
System.out.println("Meta was locked by "+localSession.getId()+" tested for "+session.getId());
if(lockExclusiveSessionStackTrace!=null){
lockExclusiveSessionStackTrace.printStackTrace();
}
}
else{
System.out.println("Meta was not locked by anyone, tested for "+session.getId());
}
return localSession == session;
}
@Override
public void unlock(Session s) {
if (database != null) {
traceLock(s, lockExclusiveSession == s, "unlock");
traceLock(s, lockExclusiveSession == s, TRACE_LOCK_UNLOCK);
if (lockExclusiveSession == s) {
lockExclusiveSession = null;
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
......
......@@ -43,8 +43,10 @@ public class TestGeneralCommonTableQueries extends TestBase {
testCreateTable();
testNestedSQL();
testRecursiveTable();
testRecursiveTableInCreateView();
testNonRecursiveTableInCreateView();
// persistent cte tests
testPersistentNonRecursiveTableInCreateView();
testPersistentRecursiveTableInCreateView();
}
private void testSimpleSelect() throws Exception {
......@@ -479,7 +481,7 @@ public class TestGeneralCommonTableQueries extends TestBase {
String[] expectedRowData =new String[]{"|meat|null","|fruit|3","|veg|2"};
String[] expectedColumnNames =new String[]{"VAL",
"SUM(SELECT\n X\nFROM PUBLIC.\"\" BB\n /* SELECT\n SUM(1) AS X,\n A\n FROM PUBLIC.B\n /++ PUBLIC.B.tableScan ++/\n /++ WHERE A IS ?1\n ++/\n /++ scanCount: 4 ++/\n INNER JOIN PUBLIC.C\n /++ PUBLIC.C.tableScan ++/\n ON 1=1\n WHERE (A IS ?1)\n AND (B.VAL = C.B)\n GROUP BY A: A IS A.VAL\n */\n /* scanCount: 1 */\nWHERE BB.A IS A.VAL)"};
int expectedNumbeOfRows = 3;
int expectedNumberOfRows = 3;
String SETUP_SQL =
"DROP TABLE IF EXISTS A; "
......@@ -516,64 +518,13 @@ public class TestGeneralCommonTableQueries extends TestBase {
+"GROUP BY A.val";
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumbeOfRows, SETUP_SQL,
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumberOfRows, SETUP_SQL,
WITH_QUERY, maxRetries-1);
}
private void testRepeatedQueryWithSetup(int maxRetries, String[] expectedRowData, String[] expectedColumnNames,
int expectedNumbeOfRows, String SETUP_SQL, String WITH_QUERY, int closeAndReopenDatabaseConnectionOnIteration) throws SQLException {
deleteDb("commonTableExpressionQueries");
Connection conn = getConnection("commonTableExpressionQueries");
PreparedStatement prep;
ResultSet rs;
for(int queryRunTries=1;queryRunTries<=maxRetries;queryRunTries++){
System.out.println("Iteration #"+queryRunTries);
Statement stat = conn.createStatement();
stat.execute(SETUP_SQL);
stat.close();
// close and re-open connection for one iteration to make sure the query work between connections
if(queryRunTries==closeAndReopenDatabaseConnectionOnIteration){
System.out.println("Reconnecting to database on iteration#"+queryRunTries+" of "+maxRetries);
conn.close();
conn = getConnection("commonTableExpressionQueries");
}
prep = conn.prepareStatement(WITH_QUERY);
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));
}
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");
}
private void testRecursiveTableInCreateView() throws Exception {
String SETUP_SQL = ""
private void testPersistentRecursiveTableInCreateView() throws Exception {
String SETUP_SQL = "--SET TRACE_LEVEL_SYSTEM_OUT 3;\n"
+"DROP TABLE IF EXISTS my_tree; \n"
+"DROP VIEW IF EXISTS v_my_tree; \n"
+"CREATE TABLE my_tree ( \n"
......@@ -615,11 +566,11 @@ public class TestGeneralCommonTableQueries extends TestBase {
"|1|2|null|121"
};
String[] expectedColumnNames =new String[]{"SUB_TREE_ROOT_ID","TREE_LEVEL","PARENT_FK","CHILD_FK"};
int expectedNumbeOfRows = 11;
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumbeOfRows, SETUP_SQL,
int expectedNumberOfRows = 11;
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumberOfRows, SETUP_SQL,
WITH_QUERY, maxRetries-1);
}
private void testNonRecursiveTableInCreateView() throws Exception {
private void testPersistentNonRecursiveTableInCreateView() throws Exception {
String SETUP_SQL = ""
+"DROP VIEW IF EXISTS v_my_nr_tree; \n"
+"DROP TABLE IF EXISTS my_table; \n"
......@@ -653,8 +604,61 @@ public class TestGeneralCommonTableQueries extends TestBase {
"|121|0|12|121",
};
String[] expectedColumnNames =new String[]{"SUB_TREE_ROOT_ID","TREE_LEVEL","PARENT_FK","CHILD_FK"};
int expectedNumbeOfRows = 5;
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumbeOfRows, SETUP_SQL,
int expectedNumberOfRows = 5;
testRepeatedQueryWithSetup(maxRetries, expectedRowData, expectedColumnNames, expectedNumberOfRows, SETUP_SQL,
WITH_QUERY, maxRetries-1);
}
private void testRepeatedQueryWithSetup(int maxRetries, String[] expectedRowData, String[] expectedColumnNames,
int expectedNumbeOfRows, String SETUP_SQL, String WITH_QUERY, int closeAndReopenDatabaseConnectionOnIteration) throws SQLException {
deleteDb("commonTableExpressionQueries");
Connection conn = getConnection("commonTableExpressionQueries");
PreparedStatement prep;
ResultSet rs;
for(int queryRunTries=1;queryRunTries<=maxRetries;queryRunTries++){
System.out.println("Iteration #"+queryRunTries);
Statement stat = conn.createStatement();
stat.execute(SETUP_SQL);
stat.close();
// close and re-open connection for one iteration to make sure the query work between connections
if(queryRunTries==closeAndReopenDatabaseConnectionOnIteration){
System.out.println("Reconnecting to database on iteration#"+queryRunTries+" of "+maxRetries);
conn.close();
conn = getConnection("commonTableExpressionQueries");
}
prep = conn.prepareStatement(WITH_QUERY);
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));
}
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");
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论