From c49db58bbc8b29d0c1cd1bbfeb086b2e3dfa74e6 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Thu, 7 Feb 2019 09:56:48 +0000 Subject: [PATCH] WL#12246, DevAPI: Prepared statement support. --- CHANGES | 2 + .../java/com/mysql/cj/x/protobuf/Mysqlx.java | 2 +- .../mysql/cj/x/protobuf/MysqlxConnection.java | 4 +- .../com/mysql/cj/x/protobuf/MysqlxCrud.java | 2 +- .../com/mysql/cj/x/protobuf/MysqlxCursor.java | 2 +- .../mysql/cj/x/protobuf/MysqlxDatatypes.java | 4 +- .../com/mysql/cj/x/protobuf/MysqlxExpect.java | 4 +- .../com/mysql/cj/x/protobuf/MysqlxExpr.java | 4 +- .../com/mysql/cj/x/protobuf/MysqlxNotice.java | 2 +- .../mysql/cj/x/protobuf/MysqlxPrepare.java | 2 +- .../mysql/cj/x/protobuf/MysqlxResultset.java | 2 +- .../mysql/cj/x/protobuf/MysqlxSession.java | 2 +- .../com/mysql/cj/x/protobuf/MysqlxSql.java | 4 +- .../com/mysql/cj/protocol/ServerSession.java | 3 +- .../com/mysql/cj/util/SequentialIdLease.java | 62 +++ .../java/com/mysql/cj/CoreSession.java | 9 +- .../java/com/mysql/cj/MysqlxSession.java | 96 +++- .../mysql/cj/protocol/x/MessageConstants.java | 6 + .../mysql/cj/protocol/x/XMessageBuilder.java | 479 +++++++++++++++--- .../com/mysql/cj/protocol/x/XProtocol.java | 125 +++++ .../com/mysql/cj/xdevapi/FilterParams.java | 13 +- .../cj/xdevapi/AbstractFilterParams.java | 12 +- .../mysql/cj/xdevapi/DeleteStatementImpl.java | 27 +- .../com/mysql/cj/xdevapi/DocFilterParams.java | 18 +- .../java/com/mysql/cj/xdevapi/ExprUtil.java | 25 +- .../mysql/cj/xdevapi/FilterableStatement.java | 8 +- .../mysql/cj/xdevapi/FindStatementImpl.java | 26 +- .../mysql/cj/xdevapi/ModifyStatementImpl.java | 35 +- .../mysql/cj/xdevapi/PreparableStatement.java | 226 +++++++++ .../mysql/cj/xdevapi/RemoveStatementImpl.java | 23 +- .../mysql/cj/xdevapi/SelectStatementImpl.java | 21 +- .../mysql/cj/xdevapi/TableFilterParams.java | 18 +- .../mysql/cj/xdevapi/UpdateStatementImpl.java | 26 +- .../simple/SequentialIdLeaseTest.java | 166 ++++++ .../x/devapi/CollectionFindTest.java | 216 ++++++++ .../x/devapi/CollectionModifyTest.java | 253 ++++++++- .../x/devapi/CollectionRemoveTest.java | 253 +++++++++ .../x/devapi/DevApiBaseTestCase.java | 84 ++- .../java/testsuite/x/devapi/SessionTest.java | 200 +++++++- .../testsuite/x/devapi/TableDeleteTest.java | 254 +++++++++- .../testsuite/x/devapi/TableSelectTest.java | 220 ++++++++ .../testsuite/x/devapi/TableUpdateTest.java | 253 ++++++++- 42 files changed, 3034 insertions(+), 159 deletions(-) create mode 100644 src/main/core-api/java/com/mysql/cj/util/SequentialIdLease.java create mode 100644 src/main/user-impl/java/com/mysql/cj/xdevapi/PreparableStatement.java create mode 100644 src/test/java/testsuite/simple/SequentialIdLeaseTest.java diff --git a/CHANGES b/CHANGES index 2e5c5ee70..1b0330489 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,8 @@ Version 8.0.16 + - WL#12246, DevAPI: Prepared statement support. + - WL#10839, Adjust c/J tests to the new "ON" default for explicit_defaults_for_timestamp. - Fix for Bug#29329326, PLEASE AVOID SHOW PROCESSLIST IF POSSIBLE. diff --git a/src/generated/java/com/mysql/cj/x/protobuf/Mysqlx.java b/src/generated/java/com/mysql/cj/x/protobuf/Mysqlx.java index f337b189e..c5621421b 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/Mysqlx.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/Mysqlx.java @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast" }) +@SuppressWarnings({ "cast" }) public final class Mysqlx { private Mysqlx() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxConnection.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxConnection.java index accd37b7c..5363b8c2e 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxConnection.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_connection.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast", "deprecation" }) +@SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxConnection { private MysqlxConnection() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCrud.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCrud.java index 021fbdd57..1adf8bd72 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCrud.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCrud.java @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_crud.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast", "deprecation" }) +@SuppressWarnings({ "deprecation", "cast" }) public final class MysqlxCrud { private MysqlxCrud() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCursor.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCursor.java index 8f25da442..783eb0910 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCursor.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxCursor.java @@ -30,7 +30,7 @@ package com.mysql.cj.x.protobuf; // Generated by the protocol buffer compiler. DO NOT EDIT! -// source: mysqlx_crud.proto +// source: mysqlx_cursor.proto @SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxCursor { diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxDatatypes.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxDatatypes.java index 6725446ad..07238435f 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxDatatypes.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxDatatypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_datatypes.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast" }) +@SuppressWarnings({ "cast" }) public final class MysqlxDatatypes { private MysqlxDatatypes() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpect.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpect.java index 3930ceb53..10f3dbd17 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpect.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpect.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_expect.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast" }) +@SuppressWarnings({ "cast" }) public final class MysqlxExpect { private MysqlxExpect() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpr.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpr.java index 24c4ddd3a..3c60d05a1 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpr.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxExpr.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_expr.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast", "deprecation" }) +@SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxExpr { private MysqlxExpr() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxNotice.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxNotice.java index 6447533dc..9e58df774 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxNotice.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxNotice.java @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_notice.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast", "deprecation" }) +@SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxNotice { private MysqlxNotice() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxPrepare.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxPrepare.java index 2bf3ac502..b6b73ceae 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxPrepare.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxPrepare.java @@ -30,7 +30,7 @@ package com.mysql.cj.x.protobuf; // Generated by the protocol buffer compiler. DO NOT EDIT! -// source: mysqlx_notice.proto +// source: mysqlx_prepare.proto @SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxPrepare { diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxResultset.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxResultset.java index ff15adfdc..e23955f6f 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxResultset.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxResultset.java @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_resultset.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast" }) +@SuppressWarnings({ "cast" }) public final class MysqlxResultset { private MysqlxResultset() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSession.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSession.java index 932a0cec8..48600c79c 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSession.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSession.java @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_session.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast" }) +@SuppressWarnings({ "cast" }) public final class MysqlxSession { private MysqlxSession() {} diff --git a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSql.java b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSql.java index 10937803b..d6b8daa85 100644 --- a/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSql.java +++ b/src/generated/java/com/mysql/cj/x/protobuf/MysqlxSql.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -31,7 +31,7 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: mysqlx_sql.proto -@SuppressWarnings({ "unchecked", "synthetic-access", "cast", "deprecation" }) +@SuppressWarnings({ "cast", "deprecation" }) public final class MysqlxSql { private MysqlxSql() {} diff --git a/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java b/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java index 907c5904b..99970b8c8 100644 --- a/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java +++ b/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -37,7 +37,6 @@ /** * Keeps the effective states of server/session variables, * contains methods for initial retrieving of these states and for their actualization. - * */ public interface ServerSession { diff --git a/src/main/core-api/java/com/mysql/cj/util/SequentialIdLease.java b/src/main/core-api/java/com/mysql/cj/util/SequentialIdLease.java new file mode 100644 index 000000000..0f6b1c737 --- /dev/null +++ b/src/main/core-api/java/com/mysql/cj/util/SequentialIdLease.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 2.0, as published by the + * Free Software Foundation. + * + * This program is also distributed with certain software (including but not + * limited to OpenSSL) that is licensed under separate terms, as designated in a + * particular file or component or in included license documentation. The + * authors of MySQL hereby grant you an additional permission to link the + * program and your derivative works with the separately licensed software that + * they have included with MySQL. + * + * Without limiting anything contained in the foregoing, this file, which is + * part of MySQL Connector/J, is also subject to the Universal FOSS Exception, + * version 1.0, a copy of which can be found at + * http://oss.oracle.com/licenses/universal-foss-exception. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, + * for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.mysql.cj.util; + +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +public class SequentialIdLease { + private Set sequentialIdsLease = new TreeSet<>(); + + /** + * Finds and allocates the first available sequential id. + * + * @return the next free sequential id. + */ + public int allocateSequentialId() { + int nextSequentialId = 0; + for (Iterator it = this.sequentialIdsLease.iterator(); it.hasNext() && nextSequentialId + 1 == it.next(); nextSequentialId++) { + // Find the first free sequential id. + } + this.sequentialIdsLease.add(++nextSequentialId); + return nextSequentialId; + } + + /** + * Frees the given sequential id so that it can be reused. + * + * @param sequentialId + * the sequential id to release + */ + public void releaseSequentialId(int sequentialId) { + this.sequentialIdsLease.remove(sequentialId); + } +} diff --git a/src/main/core-impl/java/com/mysql/cj/CoreSession.java b/src/main/core-impl/java/com/mysql/cj/CoreSession.java index 82859d9e6..7e69a1ba6 100644 --- a/src/main/core-impl/java/com/mysql/cj/CoreSession.java +++ b/src/main/core-impl/java/com/mysql/cj/CoreSession.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -35,6 +35,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -155,8 +156,12 @@ public MessageBuilder getMessageBuilder() { } public QR sendMessage(Message message) { + return sendMessage(message, this.protocol::readQueryResult); + } + + public R sendMessage(Message message, Supplier readResult) { this.protocol.send(message, 0); - return this.protocol.readQueryResult(); + return readResult.get(); } public CompletableFuture asyncSendMessage(Message message) { diff --git a/src/main/core-impl/java/com/mysql/cj/MysqlxSession.java b/src/main/core-impl/java/com/mysql/cj/MysqlxSession.java index 0afc6035c..029d1ad2c 100644 --- a/src/main/core-impl/java/com/mysql/cj/MysqlxSession.java +++ b/src/main/core-impl/java/com/mysql/cj/MysqlxSession.java @@ -47,8 +47,11 @@ import com.mysql.cj.protocol.x.StatementExecuteOkBuilder; import com.mysql.cj.protocol.x.XMessageBuilder; import com.mysql.cj.protocol.x.XProtocol; +import com.mysql.cj.protocol.x.XProtocolError; import com.mysql.cj.result.RowList; import com.mysql.cj.xdevapi.FilterParams; +import com.mysql.cj.xdevapi.FilterableStatement; +import com.mysql.cj.xdevapi.PreparableStatement; import com.mysql.cj.xdevapi.SqlDataResult; import com.mysql.cj.xdevapi.SqlResult; import com.mysql.cj.xdevapi.SqlResultImpl; @@ -90,6 +93,67 @@ public void quit() { } } + /** + * Consume an OK packet from the underlying protocol. + * + * @return null + */ + public Void readOk() { + ((XProtocol) this.protocol).readOk(); + return null; + } + + /** + * Check if current session is using a MySQL server that supports prepared statements. + * + * @return + * {@code true} if the MySQL server in use supports prepared statements + */ + public boolean supportsPreparedStatements() { + return ((XProtocol) this.protocol).supportsPreparedStatements(); + } + + /** + * Check if enough statements were executed in the underlying MySQL server so that another prepare statement attempt should be done. + * + * @return + * {@code true} if enough executions have been done since last time a prepared statement failed to be prepared + */ + public boolean readyForPreparingStatements() { + return ((XProtocol) this.protocol).readyForPreparingStatements(); + } + + /** + * Return an id to be used as a client-managed prepared statement id. + * + * @return a new identifier to be used as prepared statement id + */ + public int getNewPreparedStatementId(PreparableStatement preparableStatement) { + return ((XProtocol) this.protocol).getNewPreparedStatementId(preparableStatement); + } + + /** + * Free a prepared statement id so that it can be reused. + * + * @param preparedStatementId + * the prepared statement id to release + */ + public void freePreparedStatementId(int preparedStatementId) { + ((XProtocol) this.protocol).freePreparedStatementId(preparedStatementId); + } + + /** + * Propagate to the underlying protocol instance that preparing a statement on the connected server failed. + * + * @param preparedStatementId + * the id of the prepared statement that failed to be prepared + * @return + * {@code true} if the exception was properly handled + */ + public boolean failedPreparingStatement(int preparedStatementId, XProtocolError e) { + return ((XProtocol) this.protocol).failedPreparingStatement(preparedStatementId, e); + } + public T find(FilterParams filterParams, Function, T>> resultCtor) { this.protocol.send(((XMessageBuilder) this.messageBuilder).buildFind(filterParams), 0); @@ -99,6 +163,27 @@ public T find(FilterParams filterParams, return res; } + /** + * Execute a previously prepared find statement using the given arguments. + * + * @param preparedStatementId + * the prepared statement id to execute. This statement must be previously prepared + * @param filterParams + * the {@link FilterableStatement} params that contain the arguments for the previously-defined placeholders + * @param resultCtor + * a constructor that builds the results. + * @return + * the result from the given constructor + */ + public T executePreparedFind(int preparedStatementId, FilterParams filterParams, + Function, T>> resultCtor) { + this.protocol.send(((XMessageBuilder) this.messageBuilder).buildPrepareExecute(preparedStatementId, filterParams), 0); + ColumnDefinition metadata = this.protocol.readMetadata(); + T res = resultCtor.apply(metadata).apply(((XProtocol) this.protocol).getRowInputStream(metadata), this.protocol::readQueryResult); + this.protocol.setCurrentResultStreamer(res); + return res; + } + public CompletableFuture asyncFind(FilterParams filterParams, Function, RES_T>> resultCtor) { CompletableFuture f = new CompletableFuture<>(); @@ -109,6 +194,16 @@ public CompletableFuture asyncFind(FilterParams filterParams, public SqlResult executeSql(String sql, List args) { this.protocol.send(this.messageBuilder.buildSqlStatement(sql, args), 0); + return executeSqlProcessResult(); + } + + /** + * Process the response messages from a StmtExecute request. + * + * @return + * an {@link SqlResult} with the returned rows. + */ + private SqlResult executeSqlProcessResult() { boolean readLastResult[] = new boolean[1]; Supplier okReader = () -> { if (readLastResult[0]) { @@ -145,5 +240,4 @@ public CompletableFuture asyncExecuteSql(String sql, List arg public boolean isClosed() { return !((XProtocol) this.protocol).isOpen(); } - } diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/MessageConstants.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/MessageConstants.java index 14b0154ef..7ba786f98 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/MessageConstants.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/MessageConstants.java @@ -57,6 +57,9 @@ import com.mysql.cj.x.protobuf.MysqlxNotice.SessionStateChanged; import com.mysql.cj.x.protobuf.MysqlxNotice.SessionVariableChanged; import com.mysql.cj.x.protobuf.MysqlxNotice.Warning; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Deallocate; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Execute; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Prepare; import com.mysql.cj.x.protobuf.MysqlxResultset.ColumnMetaData; import com.mysql.cj.x.protobuf.MysqlxResultset.FetchDone; import com.mysql.cj.x.protobuf.MysqlxResultset.FetchDoneMoreResultsets; @@ -155,6 +158,9 @@ public class MessageConstants { messageClassToClientMessageType.put(ModifyView.class, ClientMessages.Type.CRUD_MODIFY_VIEW_VALUE); messageClassToClientMessageType.put(DropView.class, ClientMessages.Type.CRUD_DROP_VIEW_VALUE); messageClassToClientMessageType.put(Open.class, ClientMessages.Type.EXPECT_OPEN_VALUE); + messageClassToClientMessageType.put(Prepare.class, ClientMessages.Type.PREPARE_PREPARE_VALUE); + messageClassToClientMessageType.put(Execute.class, ClientMessages.Type.PREPARE_EXECUTE_VALUE); + messageClassToClientMessageType.put(Deallocate.class, ClientMessages.Type.PREPARE_DEALLOCATE_VALUE); MESSAGE_CLASS_TO_CLIENT_MESSAGE_TYPE = Collections.unmodifiableMap(messageClassToClientMessageType); } diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XMessageBuilder.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XMessageBuilder.java index 9cf7e51ae..402900123 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XMessageBuilder.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XMessageBuilder.java @@ -30,11 +30,11 @@ package com.mysql.cj.protocol.x; import java.security.DigestException; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -64,6 +64,7 @@ import com.mysql.cj.x.protobuf.MysqlxCrud.Insert; import com.mysql.cj.x.protobuf.MysqlxCrud.Insert.TypedRow; import com.mysql.cj.x.protobuf.MysqlxCrud.Limit; +import com.mysql.cj.x.protobuf.MysqlxCrud.LimitExpr; import com.mysql.cj.x.protobuf.MysqlxCrud.Order; import com.mysql.cj.x.protobuf.MysqlxCrud.Projection; import com.mysql.cj.x.protobuf.MysqlxCrud.Update; @@ -77,6 +78,10 @@ import com.mysql.cj.x.protobuf.MysqlxExpect; import com.mysql.cj.x.protobuf.MysqlxExpr.ColumnIdentifier; import com.mysql.cj.x.protobuf.MysqlxExpr.Expr; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Deallocate; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Execute; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Prepare; +import com.mysql.cj.x.protobuf.MysqlxPrepare.Prepare.OneOfMessage; import com.mysql.cj.x.protobuf.MysqlxSession.AuthenticateContinue; import com.mysql.cj.x.protobuf.MysqlxSession.AuthenticateStart; import com.mysql.cj.x.protobuf.MysqlxSession.Close; @@ -91,7 +96,6 @@ import com.mysql.cj.xdevapi.UpdateSpec; public class XMessageBuilder implements MessageBuilder { - private static final String XPLUGIN_NAMESPACE = "mysqlx"; public XMessage buildCapabilitiesGet() { @@ -118,6 +122,20 @@ public XMessage buildCapabilitiesSet(Map keyValuePair) { return new XMessage(CapabilitiesSet.newBuilder().setCapabilities(capsB).build()); } + /** + * Build an {@link XMessage} for a non-prepared doc insert operation. + * + * @param schemaName + * the schema name + * @param collectionName + * the collection name + * @param json + * the documents to insert + * @param upsert + * Whether this is an upsert operation or not + * @return + * an {@link XMessage} instance + */ public XMessage buildDocInsert(String schemaName, String collectionName, List json, boolean upsert) { Insert.Builder builder = Insert.newBuilder().setCollection(ExprUtil.buildCollection(schemaName, collectionName)); if (upsert != builder.getUpsert()) { @@ -127,17 +145,57 @@ public XMessage buildDocInsert(String schemaName, String collectionName, List) insertParams.getProjection()); } + return builder; + } + + /** + * Build an {@link XMessage} for a non-prepared row insert operation. + * + * @param schemaName + * the schema name + * @param tableName + * the table name + * @param insertParams + * the parameters to insert + * @return + * an {@link XMessage} instance + */ + @SuppressWarnings("unchecked") + public XMessage buildRowInsert(String schemaName, String tableName, InsertParams insertParams) { + Insert.Builder builder = commonRowInsertBuilder(schemaName, tableName, insertParams); builder.addAllRow((List) insertParams.getRows()); return new XMessage(builder.build()); } - public XMessage buildDocUpdate(FilterParams filterParams, List updates) { + /** + * Initialize an {@link Update.Builder} for collection data model with common data for prepared and non-prepared executions. + * + * @param filterParams + * the filter parameters + * @param updates + * the updates specifications to perform + * @return + * an initialized {@link Update.Builder} instance + */ + private Update.Builder commonDocUpdateBuilder(FilterParams filterParams, List updates) { Update.Builder builder = Update.newBuilder().setCollection((Collection) filterParams.getCollection()); updates.forEach(u -> { UpdateOperation.Builder opBuilder = UpdateOperation.newBuilder(); @@ -148,23 +206,112 @@ public XMessage buildDocUpdate(FilterParams filterParams, List updat } builder.addOperation(opBuilder.build()); }); + return builder; + } + + /** + * Build an {@link XMessage} for a non-prepared doc update operation. + * + * @param filterParams + * the filter parameters + * @param updates + * the updates specifications to perform + * @return + * an {@link XMessage} instance + */ + public XMessage buildDocUpdate(FilterParams filterParams, List updates) { + Update.Builder builder = commonDocUpdateBuilder(filterParams, updates); applyFilterParams(filterParams, builder::addAllOrder, builder::setLimit, builder::setCriteria, builder::addAllArgs); return new XMessage(builder.build()); } - // TODO: low-level tests of this method + /** + * Build an {@link XMessage} for a prepared doc update operation. + * + * @param preparedStatementId + * the prepared statement id + * @param filterParams + * the filter parameters + * @param updates + * the updates specifications to perform + * @return + * an {@link XMessage} instance + */ + public XMessage buildPrepareDocUpdate(int preparedStatementId, FilterParams filterParams, List updates) { + Update.Builder updateBuilder = commonDocUpdateBuilder(filterParams, updates); + applyFilterParams(filterParams, updateBuilder::addAllOrder, updateBuilder::setLimitExpr, updateBuilder::setCriteria); + + Prepare.Builder builder = Prepare.newBuilder().setStmtId(preparedStatementId); + builder.setStmt(OneOfMessage.newBuilder().setType(OneOfMessage.Type.UPDATE).setUpdate(updateBuilder.build()).build()); + return new XMessage(builder.build()); + } + + /** + * Initialize an {@link Update.Builder} for table data model with common data for prepared and non-prepared executions. + * + * @param filterParams + * the filter parameters + * @param updateParams + * the update parameters + * @return + * an initialized {@link Update.Builder} instance + */ @SuppressWarnings("unchecked") - public XMessage buildRowUpdate(FilterParams filterParams, UpdateParams updateParams) { + private Update.Builder commonRowUpdateBuilder(FilterParams filterParams, UpdateParams updateParams) { Update.Builder builder = Update.newBuilder().setDataModel(DataModel.TABLE).setCollection((Collection) filterParams.getCollection()); ((Map) updateParams.getUpdates()).entrySet().stream() .map(e -> UpdateOperation.newBuilder().setOperation(UpdateType.SET).setSource(e.getKey()).setValue(e.getValue()).build()) .forEach(builder::addOperation); + return builder; + } + + /** + * Build an {@link XMessage} for a non-prepared row update operation. + * + * @param filterParams + * the filter parameters + * @param updateParams + * the update parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildRowUpdate(FilterParams filterParams, UpdateParams updateParams) { + Update.Builder builder = commonRowUpdateBuilder(filterParams, updateParams); applyFilterParams(filterParams, builder::addAllOrder, builder::setLimit, builder::setCriteria, builder::addAllArgs); return new XMessage(builder.build()); } + /** + * Build an {@link XMessage} for a prepared row update operation. + * + * @param preparedStatementId + * the prepared statement id + * @param filterParams + * the filter parameters + * @param updateParams + * the update parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildPrepareRowUpdate(int preparedStatementId, FilterParams filterParams, UpdateParams updateParams) { + Update.Builder updateBuilder = commonRowUpdateBuilder(filterParams, updateParams); + applyFilterParams(filterParams, updateBuilder::addAllOrder, updateBuilder::setLimitExpr, updateBuilder::setCriteria); + + Prepare.Builder builder = Prepare.newBuilder().setStmtId(preparedStatementId); + builder.setStmt(OneOfMessage.newBuilder().setType(OneOfMessage.Type.UPDATE).setUpdate(updateBuilder.build()).build()); + return new XMessage(builder.build()); + } + + /** + * Initialize a {@link Find.Builder} for collection data model with common data for prepared and non-prepared executions. + * + * @param filterParams + * the filter parameters + * @return + * an initialized {@link Find.Builder} instance + */ @SuppressWarnings("unchecked") - public XMessage buildFind(FilterParams filterParams) { + private Find.Builder commonFindBuilder(FilterParams filterParams) { Find.Builder builder = Find.newBuilder().setCollection((Collection) filterParams.getCollection()); builder.setDataModel(filterParams.isRelational() ? DataModel.TABLE : DataModel.DOCUMENT); if (filterParams.getFields() != null) { @@ -182,18 +329,257 @@ public XMessage buildFind(FilterParams filterParams) { if (filterParams.getLockOption() != null) { builder.setLockingOptions(RowLockOptions.forNumber(filterParams.getLockOption().asNumber())); } + return builder; + } + + /** + * Build an {@link XMessage} for a non-prepared find operation. + * + * @param filterParams + * the filter parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildFind(FilterParams filterParams) { + Find.Builder builder = commonFindBuilder(filterParams); applyFilterParams(filterParams, builder::addAllOrder, builder::setLimit, builder::setCriteria, builder::addAllArgs); return new XMessage(builder.build()); } - public XMessage buildDelete(FilterParams filterParams) { + /** + * Build an {@link XMessage} for a prepared find operation. + * + * @param preparedStatementId + * the prepared statement id + * @param filterParams + * the filter parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildPrepareFind(int preparedStatementId, FilterParams filterParams) { + Find.Builder findBuilder = commonFindBuilder(filterParams); + applyFilterParams(filterParams, findBuilder::addAllOrder, findBuilder::setLimitExpr, findBuilder::setCriteria); + + Prepare.Builder builder = Prepare.newBuilder().setStmtId(preparedStatementId); + builder.setStmt(OneOfMessage.newBuilder().setType(OneOfMessage.Type.FIND).setFind(findBuilder.build()).build()); + return new XMessage(builder.build()); + } + + /** + * Initialize a {@link Delete.Builder} with common data for prepared and non-prepared executions. + * + * @param filterParams + * the filter parameters + * @return + * an initialized {@link Delete.Builder} instance + */ + private Delete.Builder commonDeleteBuilder(FilterParams filterParams) { Delete.Builder builder = Delete.newBuilder().setCollection((Collection) filterParams.getCollection()); + return builder; + } + + /** + * Build an {@link XMessage} for a non-prepared delete operation. + * + * @param filterParams + * the filter parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildDelete(FilterParams filterParams) { + Delete.Builder builder = commonDeleteBuilder(filterParams); applyFilterParams(filterParams, builder::addAllOrder, builder::setLimit, builder::setCriteria, builder::addAllArgs); return new XMessage(builder.build()); } - public XMessage buildClose() { - return new XMessage(Close.getDefaultInstance()); + /** + * Build an {@link XMessage} for a prepared delete operation. + * + * @param preparedStatementId + * the prepared statement id + * @param filterParams + * the filter parameters + * @return + * an {@link XMessage} instance + */ + public XMessage buildPrepareDelete(int preparedStatementId, FilterParams filterParams) { + Delete.Builder deleteBuilder = commonDeleteBuilder(filterParams); + applyFilterParams(filterParams, deleteBuilder::addAllOrder, deleteBuilder::setLimitExpr, deleteBuilder::setCriteria); + + Prepare.Builder builder = Prepare.newBuilder().setStmtId(preparedStatementId); + builder.setStmt(OneOfMessage.newBuilder().setType(OneOfMessage.Type.DELETE).setDelete(deleteBuilder.build()).build()); + return new XMessage(builder.build()); + } + + /** + * Initialize a {@link StmtExecute.Builder} with common data for prepared and non-prepared executions. + * + * @param statement + * the SQL statement + * @return + * an initialized {@link StmtExecute.Builder} instance + */ + private StmtExecute.Builder commonSqlStatementBuilder(String statement) { + StmtExecute.Builder builder = StmtExecute.newBuilder(); + // TODO: encoding (character_set_client?) + builder.setStmt(ByteString.copyFromUtf8(statement)); + return builder; + } + + /** + * Build a StmtExecute message for a SQL statement. + * + * @param statement + * SQL statement string + * @return {@link XMessage} wrapping {@link StmtExecute} + */ + public XMessage buildSqlStatement(String statement) { + return buildSqlStatement(statement, null); + } + + /** + * Build a StmtExecute message for a SQL statement. + * + * @param statement + * SQL statement string + * @param args + * list of {@link Object} arguments + * @return {@link XMessage} wrapping {@link StmtExecute} + */ + public XMessage buildSqlStatement(String statement, List args) { + StmtExecute.Builder builder = commonSqlStatementBuilder(statement); + if (args != null) { + builder.addAllArgs(args.stream().map(ExprUtil::argObjectToScalarAny).collect(Collectors.toList())); + } + return new XMessage(builder.build()); + } + + /** + * Build a Prepare message for a SQL statement. + * + * @param preparedStatementId + * the prepared statement id + * @param statement + * SQL statement string + * @return {@link XMessage} wrapping {@link StmtExecute} + */ + public XMessage buildPrepareSqlStatement(int preparedStatementId, String statement) { + StmtExecute.Builder stmtExecBuilder = commonSqlStatementBuilder(statement); + + Prepare.Builder builder = Prepare.newBuilder().setStmtId(preparedStatementId); + builder.setStmt(OneOfMessage.newBuilder().setType(OneOfMessage.Type.STMT).setStmtExecute(stmtExecBuilder.build()).build()); + return new XMessage(builder.build()); + } + + /** + * Apply the given filter params to the builder object (represented by the setter methods). + * + * Abstract the process of setting the filter params on the operation message builder. + * + * @param filterParams + * the filter params to apply + * @param setOrder + * the "builder.addAllOrder()" method reference + * @param setLimit + * the "builder.setLimit()" method reference + * @param setCriteria + * the "builder.setCriteria()" method reference + * @param setArgs + * the "builder.addAllArgs()" method reference + */ + @SuppressWarnings("unchecked") + private static void applyFilterParams(FilterParams filterParams, Consumer> setOrder, Consumer setLimit, Consumer setCriteria, + Consumer> setArgs) { + filterParams.verifyAllArgsBound(); + if (filterParams.getOrder() != null) { + setOrder.accept((List) filterParams.getOrder()); + } + if (filterParams.getLimit() != null) { + Limit.Builder lb = Limit.newBuilder().setRowCount(filterParams.getLimit()); + if (filterParams.getOffset() != null) { + lb.setOffset(filterParams.getOffset()); + } + setLimit.accept(lb.build()); + } + if (filterParams.getCriteria() != null) { + setCriteria.accept((Expr) filterParams.getCriteria()); + } + if (filterParams.getArgs() != null) { + setArgs.accept((List) filterParams.getArgs()); + } + } + + /** + * Apply the given filter params to the builder object (represented by the setter methods) using the variant that takes a LimitExpr and no + * Args. This variant is suitable for building prepared statements prepare messages. + * + * Abstract the process of setting the filter params on the operation message builder. + * + * @param filterParams + * the filter params to apply + * @param setOrder + * the "builder.addAllOrder()" method reference + * @param setLimit + * the "builder.setLimitExp()" method reference + * @param setCriteria + * the "builder.setCriteria()" method reference + */ + @SuppressWarnings("unchecked") + private static void applyFilterParams(FilterParams filterParams, Consumer> setOrder, Consumer setLimit, Consumer setCriteria) { + if (filterParams.getOrder() != null) { + setOrder.accept((List) filterParams.getOrder()); + } + Object argsList = filterParams.getArgs(); + int numberOfArgs = argsList == null ? 0 : ((List) argsList).size(); + if (filterParams.getLimit() != null) { + LimitExpr.Builder lb = LimitExpr.newBuilder().setRowCount(ExprUtil.buildPlaceholderExpr(numberOfArgs)); + if (filterParams.supportsOffset()) { + lb.setOffset(ExprUtil.buildPlaceholderExpr(numberOfArgs + 1)); + } + setLimit.accept(lb.build()); + } + if (filterParams.getCriteria() != null) { + setCriteria.accept((Expr) filterParams.getCriteria()); + } + } + + /** + * Build an {@link XMessage} for executing a prepared statement with the given filters. + * + * @param preparedStatementId + * the prepared statement id + * @param filterParams + * the filter parameter values + * @return + * an {@link XMessage} instance + */ + @SuppressWarnings("unchecked") + public XMessage buildPrepareExecute(int preparedStatementId, FilterParams filterParams) { + Execute.Builder builder = Execute.newBuilder().setStmtId(preparedStatementId); + if (filterParams.getArgs() != null) { + builder.addAllArgs(((List) filterParams.getArgs()).stream().map(s -> Any.newBuilder().setType(Any.Type.SCALAR).setScalar(s).build()) + .collect(Collectors.toList())); + } + if (filterParams.getLimit() != null) { + builder.addArgs(ExprUtil.anyOf(ExprUtil.scalarOf(filterParams.getLimit()))); + if (filterParams.supportsOffset()) { + builder.addArgs(ExprUtil.anyOf(ExprUtil.scalarOf(filterParams.getOffset() != null ? filterParams.getOffset() : 0))); + } + } + return new XMessage(builder.build()); + } + + /** + * Build an {@link XMessage} for deallocating a prepared statement. + * + * @param preparedStatementId + * the prepared statement id + * @return + * an {@link XMessage} instance + */ + public XMessage buildPrepareDeallocate(int preparedStatementId) { + Deallocate.Builder builder = Deallocate.newBuilder().setStmtId(preparedStatementId); + return new XMessage(builder.build()); } public XMessage buildCreateCollection(String schemaName, String collectionName) { @@ -227,6 +613,10 @@ public XMessage buildDropCollection(String schemaName, String collectionName) { .build())); } + public XMessage buildClose() { + return new XMessage(Close.getDefaultInstance()); + } + /** * List the objects in the given schema. Returns a table as so: * @@ -366,75 +756,6 @@ private StmtExecute buildXpluginCommand(XpluginStatementCommand command, Any... return builder.build(); } - /** - * Build a StmtExecute message for a SQL statement. - * - * @param statement - * SQL statement string - * @return {@link XMessage} wrapping {@link StmtExecute} - */ - public XMessage buildSqlStatement(String statement) { - return buildSqlStatement(statement, null); - } - - /** - * Build a StmtExecute message for a SQL statement. - * - * @param statement - * SQL statement string - * @param args - * list of {@link Object} arguments - * @return {@link XMessage} wrapping {@link StmtExecute} - */ - public XMessage buildSqlStatement(String statement, List args) { - StmtExecute.Builder builder = StmtExecute.newBuilder(); - if (args != null) { - List anyArgs = new ArrayList<>(); - args.stream().map(ExprUtil::argObjectToScalarAny).forEach(a -> anyArgs.add(a)); - builder.addAllArgs(anyArgs); - } - // TODO: encoding (character_set_client?) - builder.setStmt(ByteString.copyFromUtf8(statement)); - return new XMessage(builder.build()); - } - - /** - * Apply the given filter params to the builder object (represented by the method args). Abstract the process of setting the filter params on the operation - * message builder. - * - * @param filterParams - * the filter params to apply - * @param setOrder - * the "builder.addAllOrder()" method reference - * @param setLimit - * the "builder.setLimit()" method reference - * @param setCriteria - * the "builder.setCriteria()" method reference - * @param setArgs - * the "builder.addAllArgs()" method reference - */ - @SuppressWarnings("unchecked") - private static void applyFilterParams(FilterParams filterParams, Consumer> setOrder, Consumer setLimit, Consumer setCriteria, - Consumer> setArgs) { - filterParams.verifyAllArgsBound(); - if (filterParams.getOrder() != null) { - setOrder.accept((List) filterParams.getOrder()); - } - if (filterParams.getLimit() != null) { - Limit.Builder lb = Limit.newBuilder().setRowCount(filterParams.getLimit()); - if (filterParams.getOffset() != null) { - lb.setOffset(filterParams.getOffset()); - } - setLimit.accept(lb.build()); - } - if (filterParams.getCriteria() != null) { - setCriteria.accept((Expr) filterParams.getCriteria()); - } - if (filterParams.getArgs() != null) { - setArgs.accept((List) filterParams.getArgs()); - } - } - public XMessage buildSha256MemoryAuthStart() { return new XMessage(AuthenticateStart.newBuilder().setMechName("SHA256_MEMORY").build()); } diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XProtocol.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XProtocol.java index d94f958ed..4b1fdd1a9 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XProtocol.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/x/XProtocol.java @@ -34,12 +34,15 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; import java.nio.channels.CompletionHandler; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.concurrent.CompletableFuture; import com.mysql.cj.CharsetMapping; @@ -63,6 +66,7 @@ import com.mysql.cj.exceptions.ExceptionFactory; import com.mysql.cj.exceptions.ExceptionInterceptor; import com.mysql.cj.exceptions.FeatureNotAvailableException; +import com.mysql.cj.exceptions.MysqlErrorNumbers; import com.mysql.cj.exceptions.SSLParamsException; import com.mysql.cj.exceptions.WrongArgumentException; import com.mysql.cj.protocol.AbstractProtocol; @@ -86,6 +90,7 @@ import com.mysql.cj.result.DefaultColumnDefinition; import com.mysql.cj.result.Field; import com.mysql.cj.result.LongValueFactory; +import com.mysql.cj.util.SequentialIdLease; import com.mysql.cj.util.StringUtils; import com.mysql.cj.x.protobuf.Mysqlx.ServerMessages; import com.mysql.cj.x.protobuf.MysqlxConnection.Capabilities; @@ -94,12 +99,15 @@ import com.mysql.cj.x.protobuf.MysqlxResultset.Row; import com.mysql.cj.x.protobuf.MysqlxSession.AuthenticateContinue; import com.mysql.cj.xdevapi.FilterParams; +import com.mysql.cj.xdevapi.PreparableStatement; +import com.mysql.cj.xdevapi.PreparableStatement.PreparableStatementFinalizer; import com.mysql.cj.xdevapi.SqlResult; /** * Low-level interface to communications with X Plugin. */ public class XProtocol extends AbstractProtocol implements Protocol { + private static int RETRY_PREPARE_STATEMENT_COUNTDOWN = 100; private MessageReader reader; private MessageSender sender; @@ -117,6 +125,13 @@ public class XProtocol extends AbstractProtocol implements Protocol clientCapabilities = new HashMap<>(); + /** Keeps track of whether this X Server session supports prepared statements. True by default until first failure of a statement prepare. */ + private boolean supportsPreparedStatements = true; + private int retryPrepareStatementCountdown = 0; + private SequentialIdLease preparedStatementIds = new SequentialIdLease(); + private ReferenceQueue> preparableStatementRefQueue = new ReferenceQueue<>(); + private Map preparableStatementFinalizerReferences = new TreeMap<>(); + public XProtocol(String host, int port, String defaultSchema, PropertySet propertySet) { this.defaultSchemaName = defaultSchema; @@ -546,6 +561,90 @@ public XProtocolRowInputStream getRowInputStream(ColumnDefinition metadata) { return new XProtocolRowInputStream(metadata, this); } + /** + * Checks if the MySQL server currently connected supports prepared statements. + * + * @return + * {@code true} if the MySQL server currently connected supports prepared statements. + */ + public boolean supportsPreparedStatements() { + return this.supportsPreparedStatements; + } + + /** + * Checks if enough statements have been executed in this MySQL server so that another prepare statement attempt should be done. + * + * @return + * {@code true} if enough executions have been done since last time a prepared statement failed to prepare + */ + public boolean readyForPreparingStatements() { + if (this.retryPrepareStatementCountdown == 0) { + return true; + } + this.retryPrepareStatementCountdown--; + return false; + } + + /** + * Returns an id to be used as a client-managed prepared statement id. The method {@link #freePreparedStatementId(int)} must be called when the prepared + * statement is deallocated so that the same id can be re-used. + * + * @return a new identifier to be used as prepared statement id + */ + public int getNewPreparedStatementId(PreparableStatement preparableStatement) { + if (!this.supportsPreparedStatements) { + throw new XProtocolError("The connected MySQL server does not support prepared statements."); + } + int preparedStatementId = this.preparedStatementIds.allocateSequentialId(); + this.preparableStatementFinalizerReferences.put(preparedStatementId, + new PreparableStatementFinalizer(preparableStatement, this.preparableStatementRefQueue, preparedStatementId)); + return preparedStatementId; + } + + /** + * Frees a prepared statement id so that it can be reused. Note that freeing an id from an active prepared statement will result in a statement prepare + * conflict next time one gets prepared with the same released id. + * + * @param preparedStatementId + * the prepared statement id to release + */ + public void freePreparedStatementId(int preparedStatementId) { + if (!this.supportsPreparedStatements) { + throw new XProtocolError("The connected MySQL server does not support prepared statements."); + } + this.preparedStatementIds.releaseSequentialId(preparedStatementId); + this.preparableStatementFinalizerReferences.remove(preparedStatementId); + } + + /** + * Informs this protocol instance that preparing a statement on the connected server failed. + * + * @param preparedStatementId + * the id of the prepared statement that failed to prepare + * @return + * {@code true} if the exception was properly handled + */ + public boolean failedPreparingStatement(int preparedStatementId, XProtocolError e) { + freePreparedStatementId(preparedStatementId); + + if (e.getErrorCode() == MysqlErrorNumbers.ER_MAX_PREPARED_STMT_COUNT_REACHED) { + this.retryPrepareStatementCountdown = RETRY_PREPARE_STATEMENT_COUNTDOWN; + return true; + } + + if (e.getErrorCode() == MysqlErrorNumbers.ER_UNKNOWN_COM_ERROR && this.preparableStatementFinalizerReferences.isEmpty()) { + // The server doesn't recognize the protocol message, so it doesn't support prepared statements. + this.supportsPreparedStatements = false; + this.retryPrepareStatementCountdown = 0; + this.preparedStatementIds = null; + this.preparableStatementRefQueue = null; + this.preparableStatementFinalizerReferences = null; + return true; + } + + return false; + } + /** * Signal the intent to start processing a new command. A session supports processing a single command at a time. Results are reading lazily from the * wire. It is necessary to flush any pending result before starting a new command. This method performs the flush if necessary. @@ -559,6 +658,25 @@ protected void newCommand() { this.currentResultStreamer = null; } } + + // Before continuing clean up any abandoned prepared statements that were not properly deallocated. + if (this.supportsPreparedStatements) { + Reference> ref; + while ((ref = this.preparableStatementRefQueue.poll()) != null) { + PreparableStatementFinalizer psf = (PreparableStatementFinalizer) ref; + psf.clear(); + try { + this.sender.send(((XMessageBuilder) this.messageBuilder).buildPrepareDeallocate(psf.getPreparedStatementId())); + readOk(); + } catch (XProtocolError e) { + if (e.getErrorCode() != MysqlErrorNumbers.ER_X_BAD_STATEMENT_ID) { + throw e; + } // Else ignore exception, the Statement may have been deallocated elsewhere. + } finally { + freePreparedStatementId(psf.getPreparedStatementId()); + } + } + } } public void setCurrentResultStreamer(ResultStreamer currentResultStreamer) { @@ -707,6 +825,13 @@ public void reset() { this.authProvider.changeUser(null, this.currUser, this.currPassword, this.currDatabase); } + // No prepared statements survived to Mysqlx.Session.Reset. Reset all related control structures. + if (this.supportsPreparedStatements) { + this.retryPrepareStatementCountdown = 0; + this.preparedStatementIds = new SequentialIdLease(); + this.preparableStatementRefQueue = new ReferenceQueue<>(); + this.preparableStatementFinalizerReferences = new TreeMap<>(); + } } @Override diff --git a/src/main/user-api/java/com/mysql/cj/xdevapi/FilterParams.java b/src/main/user-api/java/com/mysql/cj/xdevapi/FilterParams.java index 8a2c6c8f4..6a4425ab4 100644 --- a/src/main/user-api/java/com/mysql/cj/xdevapi/FilterParams.java +++ b/src/main/user-api/java/com/mysql/cj/xdevapi/FilterParams.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -167,6 +167,17 @@ public int asNumber() { */ void setOffset(Long offset); + /** + * Whether offset clause is supported in the statement or not. + *

+ * Note that setting offset values is always possible, even if they are not supported. + *

+ * + * @return + * true if offset clause is supported + */ + boolean supportsOffset(); + /** * Get the search criteria. * diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/AbstractFilterParams.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/AbstractFilterParams.java index d9d8a170d..fd81e2d5b 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/AbstractFilterParams.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/AbstractFilterParams.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -49,6 +49,7 @@ public abstract class AbstractFilterParams implements FilterParams { protected Collection collection; protected Long limit; protected Long offset; + protected boolean supportsOffset; protected String[] orderExpr; private List order; protected String criteriaStr; @@ -73,11 +74,14 @@ public abstract class AbstractFilterParams implements FilterParams { * Schema name * @param collectionName * Collection name + * @param supportsOffset + * Whether offset is supported or not * @param isRelational * Are relational columns identifiers allowed? */ - public AbstractFilterParams(String schemaName, String collectionName, boolean isRelational) { + public AbstractFilterParams(String schemaName, String collectionName, boolean supportsOffset, boolean isRelational) { this.collection = ExprUtil.buildCollection(schemaName, collectionName); + this.supportsOffset = supportsOffset; this.isRelational = isRelational; } @@ -112,6 +116,10 @@ public void setOffset(Long offset) { this.offset = offset; } + public boolean supportsOffset() { + return this.supportsOffset; + } + public Object getCriteria() { return this.criteria; } diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/DeleteStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/DeleteStatementImpl.java index 5ca66682d..8d15b0186 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/DeleteStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/DeleteStatementImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -34,28 +34,35 @@ import com.mysql.cj.MysqlxSession; import com.mysql.cj.protocol.x.StatementExecuteOk; import com.mysql.cj.protocol.x.XMessage; -import com.mysql.cj.protocol.x.XMessageBuilder; /** * {@link DeleteStatement} implementation. */ public class DeleteStatementImpl extends FilterableStatement implements DeleteStatement { - private MysqlxSession mysqlxSession; - /* package private */ DeleteStatementImpl(MysqlxSession mysqlxSession, String schema, String table) { - super(new TableFilterParams(schema, table)); + super(new TableFilterParams(schema, table, false)); this.mysqlxSession = mysqlxSession; } - public Result execute() { - StatementExecuteOk ok = this.mysqlxSession - .sendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildDelete(this.filterParams)); + @Override + protected Result executeStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildDelete(this.filterParams)); + return new UpdateResult(ok); + } + + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareDelete(this.preparedStatementId, this.filterParams); + } + + @Override + protected Result executePreparedStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildPrepareExecute(this.preparedStatementId, this.filterParams)); return new UpdateResult(ok); } public CompletableFuture executeAsync() { - CompletableFuture okF = this.mysqlxSession - .asyncSendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildDelete(this.filterParams)); + CompletableFuture okF = this.mysqlxSession.asyncSendMessage(getMessageBuilder().buildDelete(this.filterParams)); return okF.thenApply(ok -> new UpdateResult(ok)); } } diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/DocFilterParams.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/DocFilterParams.java index c19b24245..ad13a575b 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/DocFilterParams.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/DocFilterParams.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -49,7 +49,21 @@ public class DocFilterParams extends AbstractFilterParams { * Collection name */ public DocFilterParams(String schemaName, String collectionName) { - super(schemaName, collectionName, false); + this(schemaName, collectionName, true); + } + + /** + * Constructor. + * + * @param schemaName + * Schema name + * @param collectionName + * Collection name + * @param supportsOffset + * Whether OFFSET is supported or not + */ + public DocFilterParams(String schemaName, String collectionName, boolean supportsOffset) { + super(schemaName, collectionName, supportsOffset, false); } /** diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/ExprUtil.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/ExprUtil.java index 4d0f41378..067606725 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/ExprUtil.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/ExprUtil.java @@ -132,6 +132,17 @@ public static Expr buildLiteralExpr(Scalar scalar) { return Expr.newBuilder().setType(Expr.Type.LITERAL).setLiteral(scalar).build(); } + /** + * Creates a placeholder expression for the given position in the args array + * + * @param pos + * the position of the placeholder in the args array + * @return {@link Expr} + */ + public static Expr buildPlaceholderExpr(int pos) { + return Expr.newBuilder().setType(Expr.Type.PLACEHOLDER).setPosition(pos).build(); + } + /** * Protocol buffers helper to build a Scalar NULL type. * @@ -198,6 +209,17 @@ public static Scalar scalarOf(boolean b) { return Scalar.newBuilder().setType(Scalar.Type.V_BOOL).setVBool(b).build(); } + /** + * Protocol buffers helper to build an Any Scalar type. + * + * @param s + * value + * @return {@link Any} + */ + public static Any anyOf(Scalar s) { + return Any.newBuilder().setType(Any.Type.SCALAR).setScalar(s).build(); + } + /** * Build a Protocol buffers Any with a string value. * @@ -209,8 +231,7 @@ public static Any buildAny(String str) { // same as Expr Scalar.String sstr = Scalar.String.newBuilder().setValue(ByteString.copyFromUtf8(str)).build(); Scalar s = Scalar.newBuilder().setType(Scalar.Type.V_STRING).setVString(sstr).build(); - Any a = Any.newBuilder().setType(Any.Type.SCALAR).setScalar(s).build(); - return a; + return anyOf(s); } /** diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/FilterableStatement.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/FilterableStatement.java index 9a9369265..d8aa2be6d 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/FilterableStatement.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/FilterableStatement.java @@ -37,7 +37,7 @@ * @param * result interface */ -public abstract class FilterableStatement implements Statement { +public abstract class FilterableStatement extends PreparableStatement implements Statement { protected FilterParams filterParams; /** @@ -63,6 +63,7 @@ public FilterableStatement(FilterParams filterParams) { */ @SuppressWarnings("unchecked") public STMT_T where(String searchCondition) { + resetPrepareState(); this.filterParams.setCriteria(searchCondition); return (STMT_T) this; } @@ -97,6 +98,7 @@ public STMT_T sort(String... sortFields) { */ @SuppressWarnings({ "unchecked", "deprecation" }) // Suppress deprecation warning is required until RemoveStatement.orderBy is removed. public STMT_T orderBy(String... sortFields) { + resetPrepareState(); this.filterParams.setOrder(sortFields); return (STMT_T) this; } @@ -118,6 +120,9 @@ public STMT_T orderBy(String... sortFields) { */ @SuppressWarnings("unchecked") public STMT_T limit(long numberOfRows) { + if (this.filterParams.getLimit() == null) { + setReprepareState(); + } this.filterParams.setLimit(numberOfRows); return (STMT_T) this; } @@ -139,6 +144,7 @@ public STMT_T limit(long numberOfRows) { */ @SuppressWarnings("unchecked") public STMT_T offset(long limitOffset) { + // Offset depends on Limit. There is no need to re-prepare the statement even if OFFSET was never set before. this.filterParams.setOffset(limitOffset); return (STMT_T) this; } diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/FindStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/FindStatementImpl.java index dd546c183..cebf65ce8 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/FindStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/FindStatementImpl.java @@ -32,6 +32,7 @@ import java.util.concurrent.CompletableFuture; import com.mysql.cj.MysqlxSession; +import com.mysql.cj.protocol.x.XMessage; import com.mysql.cj.xdevapi.FilterParams.RowLock; import com.mysql.cj.xdevapi.FilterParams.RowLockOptions; @@ -39,42 +40,59 @@ * {@link FindStatement} implementation. */ public class FindStatementImpl extends FilterableStatement implements FindStatement { - private MysqlxSession mysqlxSession; - /* package private */ FindStatementImpl(MysqlxSession mysqlxSession, String schema, String collection, String criteria) { super(new DocFilterParams(schema, collection)); this.mysqlxSession = mysqlxSession; if (criteria != null && criteria.length() > 0) { this.filterParams.setCriteria(criteria); } + if (!this.mysqlxSession.supportsPreparedStatements()) { + this.preparedState = PreparedState.UNSUPPORTED; + } } - public DocResultImpl execute() { + @Override + protected DocResultImpl executeStatement() { return this.mysqlxSession.find(this.filterParams, metadata -> (rows, task) -> new DocResultImpl(rows, task, this.mysqlxSession.getPropertySet())); } + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareFind(this.preparedStatementId, this.filterParams); + } + + @Override + protected DocResultImpl executePreparedStatement() { + return this.mysqlxSession.executePreparedFind(this.preparedStatementId, this.filterParams, + metadata -> (rows, task) -> new DocResultImpl(rows, task, this.mysqlxSession.getPropertySet())); + } + public CompletableFuture executeAsync() { return this.mysqlxSession.asyncFind(this.filterParams, metadata -> (rows, task) -> new DocResultImpl(rows, task, this.mysqlxSession.getPropertySet())); } @Override public FindStatement fields(String... projection) { + resetPrepareState(); this.filterParams.setFields(projection); return this; } public FindStatement fields(Expression docProjection) { + resetPrepareState(); ((DocFilterParams) this.filterParams).setFields(docProjection); return this; } @Override public FindStatement groupBy(String... groupBy) { + resetPrepareState(); this.filterParams.setGrouping(groupBy); return this; } public FindStatement having(String having) { + resetPrepareState(); this.filterParams.setGroupingCriteria(having); return this; } @@ -86,6 +104,7 @@ public FindStatement lockShared() { @Override public FindStatement lockShared(LockContention lockContention) { + resetPrepareState(); this.filterParams.setLock(RowLock.SHARED_LOCK); switch (lockContention) { case NOWAIT: @@ -106,6 +125,7 @@ public FindStatement lockExclusive() { @Override public FindStatement lockExclusive(LockContention lockContention) { + resetPrepareState(); this.filterParams.setLock(RowLock.EXCLUSIVE_LOCK); switch (lockContention) { case NOWAIT: diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/ModifyStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/ModifyStatementImpl.java index 354b474aa..348499913 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/ModifyStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/ModifyStatementImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -45,69 +45,88 @@ * {@link ModifyStatement} implementation. */ public class ModifyStatementImpl extends FilterableStatement implements ModifyStatement { - private MysqlxSession mysqlxSession; private List updates = new ArrayList<>(); /* package private */ ModifyStatementImpl(MysqlxSession mysqlxSession, String schema, String collection, String criteria) { - super(new DocFilterParams(schema, collection)); + super(new DocFilterParams(schema, collection, false)); + this.mysqlxSession = mysqlxSession; if (criteria == null || criteria.trim().length() == 0) { throw new XDevAPIError(Messages.getString("ModifyStatement.0", new String[] { "criteria" })); } this.filterParams.setCriteria(criteria); - this.mysqlxSession = mysqlxSession; + if (!this.mysqlxSession.supportsPreparedStatements()) { + this.preparedState = PreparedState.UNSUPPORTED; + } + } + + @Override + protected Result executeStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildDocUpdate(this.filterParams, this.updates)); + return new UpdateResult(ok); + } + + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareDocUpdate(this.preparedStatementId, this.filterParams, this.updates); } @Override - public Result execute() { - StatementExecuteOk ok = this.mysqlxSession - .sendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildDocUpdate(this.filterParams, this.updates)); + protected Result executePreparedStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildPrepareExecute(this.preparedStatementId, this.filterParams)); return new UpdateResult(ok); } @Override public CompletableFuture executeAsync() { CompletableFuture okF = this.mysqlxSession - .asyncSendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildDocUpdate(this.filterParams, this.updates)); + .asyncSendMessage(((XMessageBuilder) this.mysqlxSession.getMessageBuilder()).buildDocUpdate(this.filterParams, this.updates)); return okF.thenApply(ok -> new UpdateResult(ok)); } @Override public ModifyStatement set(String docPath, Object value) { + resetPrepareState(); this.updates.add(new UpdateSpec(UpdateType.ITEM_SET, docPath).setValue(value)); return this; } @Override public ModifyStatement change(String docPath, Object value) { + resetPrepareState(); this.updates.add(new UpdateSpec(UpdateType.ITEM_REPLACE, docPath).setValue(value)); return this; } @Override public ModifyStatement unset(String... fields) { + resetPrepareState(); this.updates.addAll(Arrays.stream(fields).map(docPath -> new UpdateSpec(UpdateType.ITEM_REMOVE, docPath)).collect(Collectors.toList())); return this; } @Override public ModifyStatement patch(DbDoc document) { + resetPrepareState(); return patch(document.toString()); } @Override public ModifyStatement patch(String document) { + resetPrepareState(); this.updates.add(new UpdateSpec(UpdateType.MERGE_PATCH, "").setValue(Expression.expr(document))); return this; } @Override public ModifyStatement arrayInsert(String field, Object value) { + resetPrepareState(); this.updates.add(new UpdateSpec(UpdateType.ARRAY_INSERT, field).setValue(value)); return this; } @Override public ModifyStatement arrayAppend(String docPath, Object value) { + resetPrepareState(); this.updates.add(new UpdateSpec(UpdateType.ARRAY_APPEND, docPath).setValue(value)); return this; } diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/PreparableStatement.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/PreparableStatement.java new file mode 100644 index 000000000..673124d57 --- /dev/null +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/PreparableStatement.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 2.0, as published by the + * Free Software Foundation. + * + * This program is also distributed with certain software (including but not + * limited to OpenSSL) that is licensed under separate terms, as designated in a + * particular file or component or in included license documentation. The + * authors of MySQL hereby grant you an additional permission to link the + * program and your derivative works with the separately licensed software that + * they have included with MySQL. + * + * Without limiting anything contained in the foregoing, this file, which is + * part of MySQL Connector/J, is also subject to the Universal FOSS Exception, + * version 1.0, a copy of which can be found at + * http://oss.oracle.com/licenses/universal-foss-exception. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, + * for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.mysql.cj.xdevapi; + +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; + +import com.mysql.cj.MysqlxSession; +import com.mysql.cj.protocol.x.XMessage; +import com.mysql.cj.protocol.x.XMessageBuilder; +import com.mysql.cj.protocol.x.XProtocolError; + +/** + * Abstract class, common to all X DevAPI statement classes that can be prepared. + * + * @param + * result interface + */ +public abstract class PreparableStatement { + protected enum PreparedState { + UNSUPPORTED, // Preparing statements is completely unsupported in the server currently being used. + UNPREPARED, // Statement is not prepared yet, next execution will run unprepared. + SUSPENDED, // Preparing statements is currently suspended but it is expected to resume sometime later. + PREPARED, // The statement is prepared and ready for execution. + PREPARE, // The statement shall be prepared on next execution. + DEALLOCATE, // The statement shall be deallocated on next execution. + REPREPARE; // The statement shall be deallocated and immediately re-prepared on next execution. + } + + protected int preparedStatementId = 0; + protected PreparedState preparedState = PreparedState.UNPREPARED; + + protected MysqlxSession mysqlxSession; + + /** + * Helper method to return an {@link XMessageBuilder} instance from {@link MysqlxSession} in use. + * + * @return + * the {@link XMessageBuilder} instance from current {@link MysqlxSession} + */ + protected XMessageBuilder getMessageBuilder() { + return (XMessageBuilder) this.mysqlxSession.getMessageBuilder(); + } + + /** + * Mark this preparable statement to be deallocated on next execution, if it is currently prepared, or cancel the next prepare. + */ + protected void resetPrepareState() { + if (this.preparedState == PreparedState.PREPARED || this.preparedState == PreparedState.REPREPARE) { + this.preparedState = PreparedState.DEALLOCATE; + } else if (this.preparedState == PreparedState.PREPARE) { + this.preparedState = PreparedState.UNPREPARED; + } + } + + /** + * Mark this preparable statement to be deallocated and re-prepared on next execution, if it is currently prepared. + */ + protected void setReprepareState() { + if (this.preparedState == PreparedState.PREPARED) { + this.preparedState = PreparedState.REPREPARE; + } + } + + /** + * Executes synchronously this statement either directly or using prepared statements if: + * 1. Prepared statements are supported by the server. + * 2. The statement is executed repeatedly without changing its structure. + * + * @return + * the object returned from the low level statement execution + */ + public RES_T execute() { + for (;;) { + switch (this.preparedState) { + case UNSUPPORTED: + // Fall-back to non-prepared statement execution. + return executeStatement(); + case UNPREPARED: + // Execute as non-prepared this time but mark as to be prepared on next execution. + RES_T result = executeStatement(); + this.preparedState = PreparedState.PREPARE; + return result; + case SUSPENDED: + // An error occurred in some previous prepare. If the server doesn't support prepared statements then mark it as unsupported, otherwise wait + // until the server is ready to accept new prepare attempts, executing non-prepared in the meantime. + if (!this.mysqlxSession.supportsPreparedStatements()) { + this.preparedState = PreparedState.UNSUPPORTED; + } else if (this.mysqlxSession.readyForPreparingStatements()) { + this.preparedState = PreparedState.PREPARE; + } else { + return executeStatement(); + } + break; + case PREPARE: + // Prepare this statement. If it succeeds then immediately follow with its execution, otherwise mark it as prepare suspended and let the + // following iteration to decide what to do next. + this.preparedState = prepareStatement() ? PreparedState.PREPARED : PreparedState.SUSPENDED; + break; + case PREPARED: + // The statement is already prepared and can be executed safely. + return executePreparedStatement(); + case DEALLOCATE: + // Deallocate this statement and set it as unprepared so that it may be prepared later again. + deallocatePrepared(); + this.preparedState = PreparedState.UNPREPARED; + break; + case REPREPARE: + // Deallocate this statement and set it as to prepare so that it gets prepared and executed right away. + deallocatePrepared(); + this.preparedState = PreparedState.PREPARE; + break; + } + } + } + + /** + * Executes the statement directly (non-prepared). Implementation is dependent on the statement type. + * + * @return + * the object returned from the lower level statement execution + */ + protected abstract RES_T executeStatement(); + + /** + * Returns the {@link XMessage} needed to prepare this statement. Implementation is dependent on the statement type. + * + * @return + * the {@link XMessage} that prepares this statement + */ + protected abstract XMessage getPrepareStatementXMessage(); + + /** + * Prepares a statement on the server to be later executed. + * + * @return + * true if the statement was successfully prepared, false otherwise + */ + private boolean prepareStatement() { + if (!this.mysqlxSession.supportsPreparedStatements()) { + return false; + } + try { + this.preparedStatementId = this.mysqlxSession.getNewPreparedStatementId(this); + this.mysqlxSession.sendMessage(getPrepareStatementXMessage(), this.mysqlxSession::readOk); + } catch (XProtocolError e) { + if (this.mysqlxSession.failedPreparingStatement(this.preparedStatementId, e)) { + this.preparedStatementId = 0; + return false; + } + this.preparedStatementId = 0; + throw e; + } catch (Throwable t) { + this.preparedStatementId = 0; + throw t; + } + return true; + } + + /** + * Executes a previously server-prepared statement. Implementation is dependent on the statement type. + * + * @return + * the object returned from the lower level statement execution + */ + protected abstract RES_T executePreparedStatement(); + + /** + * Deallocate this prepared statement from current {@link MysqlxSession}. + */ + protected void deallocatePrepared() { + if (this.preparedState == PreparedState.PREPARED || this.preparedState == PreparedState.DEALLOCATE || this.preparedState == PreparedState.REPREPARE) { + try { + this.mysqlxSession.sendMessage(getMessageBuilder().buildPrepareDeallocate(this.preparedStatementId), this.mysqlxSession::readOk); + } finally { + this.mysqlxSession.freePreparedStatementId(this.preparedStatementId); + this.preparedStatementId = 0; + } + } + } + + /** + * {@link PhantomReference} to track prepared statement ids. An instance of this class must be kept until the prepared statement is properly deallocated. If + * proper deallocation does not happen, this is used to identify abandoned prepared statements and proceed with its deallocation after the object is + * destructed by using a {@link ReferenceQueue}. + */ + public static class PreparableStatementFinalizer extends PhantomReference> { + int prepredStatementId; + + public PreparableStatementFinalizer(PreparableStatement referent, ReferenceQueue> q, int preparedStatementId) { + super(referent, q); + this.prepredStatementId = preparedStatementId; + } + + public int getPreparedStatementId() { + return this.prepredStatementId; + } + } +} diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/RemoveStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/RemoveStatementImpl.java index 480dbc120..b94d9b64b 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/RemoveStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/RemoveStatementImpl.java @@ -41,20 +41,29 @@ * {@link RemoveStatement} implementation. */ public class RemoveStatementImpl extends FilterableStatement implements RemoveStatement { - private MysqlxSession mysqlxSession; - /* package private */ RemoveStatementImpl(MysqlxSession mysqlxSession, String schema, String collection, String criteria) { - super(new DocFilterParams(schema, collection)); + super(new DocFilterParams(schema, collection, false)); + this.mysqlxSession = mysqlxSession; if (criteria == null || criteria.trim().length() == 0) { throw new XDevAPIError(Messages.getString("RemoveStatement.0", new String[] { "criteria" })); } this.filterParams.setCriteria(criteria); - this.mysqlxSession = mysqlxSession; } - public Result execute() { - StatementExecuteOk ok = this.mysqlxSession - .sendMessage(((XMessageBuilder) this.mysqlxSession.getMessageBuilder()).buildDelete(this.filterParams)); + @Override + public Result executeStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildDelete(this.filterParams)); + return new UpdateResult(ok); + } + + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareDelete(this.preparedStatementId, this.filterParams); + } + + @Override + protected Result executePreparedStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildPrepareExecute(this.preparedStatementId, this.filterParams)); return new UpdateResult(ok); } diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/SelectStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/SelectStatementImpl.java index 21fb696b2..da7f45496 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/SelectStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/SelectStatementImpl.java @@ -32,6 +32,7 @@ import java.util.concurrent.CompletableFuture; import com.mysql.cj.MysqlxSession; +import com.mysql.cj.protocol.x.XMessage; import com.mysql.cj.xdevapi.FilterParams.RowLock; import com.mysql.cj.xdevapi.FilterParams.RowLockOptions; @@ -39,8 +40,6 @@ * {@link SelectStatement} implementation. */ public class SelectStatementImpl extends FilterableStatement implements SelectStatement { - private MysqlxSession mysqlxSession; - /* package private */ SelectStatementImpl(MysqlxSession mysqlxSession, String schema, String table, String... projection) { super(new TableFilterParams(schema, table)); this.mysqlxSession = mysqlxSession; @@ -49,11 +48,23 @@ public class SelectStatementImpl extends FilterableStatement (rows, task) -> new RowResultImpl(metadata, this.mysqlxSession.getServerSession().getDefaultTimeZone(), rows, task, this.mysqlxSession.getPropertySet())); } + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareFind(this.preparedStatementId, this.filterParams); + } + + @Override + protected RowResultImpl executePreparedStatement() { + return this.mysqlxSession.executePreparedFind(this.preparedStatementId, this.filterParams, metadata -> (rows, task) -> new RowResultImpl(metadata, + this.mysqlxSession.getServerSession().getDefaultTimeZone(), rows, task, this.mysqlxSession.getPropertySet())); + } + public CompletableFuture executeAsync() { return this.mysqlxSession.asyncFind(this.filterParams, metadata -> (rows, task) -> new RowResultImpl(metadata, this.mysqlxSession.getServerSession().getDefaultTimeZone(), rows, task, this.mysqlxSession.getPropertySet())); @@ -61,11 +72,13 @@ public CompletableFuture executeAsync() { @Override public SelectStatement groupBy(String... groupBy) { + resetPrepareState(); this.filterParams.setGrouping(groupBy); return this; } public SelectStatement having(String having) { + resetPrepareState(); this.filterParams.setGroupingCriteria(having); return this; } @@ -82,6 +95,7 @@ public SelectStatement lockShared() { @Override public SelectStatement lockShared(LockContention lockContention) { + resetPrepareState(); this.filterParams.setLock(RowLock.SHARED_LOCK); switch (lockContention) { case NOWAIT: @@ -102,6 +116,7 @@ public SelectStatement lockExclusive() { @Override public SelectStatement lockExclusive(LockContention lockContention) { + resetPrepareState(); this.filterParams.setLock(RowLock.EXCLUSIVE_LOCK); switch (lockContention) { case NOWAIT: diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/TableFilterParams.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/TableFilterParams.java index 43249f7df..143b195c4 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/TableFilterParams.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/TableFilterParams.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -45,7 +45,21 @@ public class TableFilterParams extends AbstractFilterParams { * Collection name */ public TableFilterParams(String schemaName, String collectionName) { - super(schemaName, collectionName, true); + this(schemaName, collectionName, true); + } + + /** + * Constructor. + * + * @param schemaName + * Schema name + * @param collectionName + * Collection name + * @param supportsOffset + * Whether offset is supported or not + */ + public TableFilterParams(String schemaName, String collectionName, boolean supportsOffset) { + super(schemaName, collectionName, supportsOffset, true); } @Override diff --git a/src/main/user-impl/java/com/mysql/cj/xdevapi/UpdateStatementImpl.java b/src/main/user-impl/java/com/mysql/cj/xdevapi/UpdateStatementImpl.java index 0ee06a750..e6367f0b4 100644 --- a/src/main/user-impl/java/com/mysql/cj/xdevapi/UpdateStatementImpl.java +++ b/src/main/user-impl/java/com/mysql/cj/xdevapi/UpdateStatementImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -41,32 +41,44 @@ * {@link UpdateStatement} implementation. */ public class UpdateStatementImpl extends FilterableStatement implements UpdateStatement { - private MysqlxSession mysqlxSession; private UpdateParams updateParams = new UpdateParams(); /* package private */ UpdateStatementImpl(MysqlxSession mysqlxSession, String schema, String table) { - super(new TableFilterParams(schema, table)); + super(new TableFilterParams(schema, table, false)); this.mysqlxSession = mysqlxSession; } - public Result execute() { - StatementExecuteOk ok = this.mysqlxSession - .sendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildRowUpdate(this.filterParams, this.updateParams)); + @Override + protected Result executeStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildRowUpdate(this.filterParams, this.updateParams)); + return new UpdateResult(ok); + } + + @Override + protected XMessage getPrepareStatementXMessage() { + return getMessageBuilder().buildPrepareRowUpdate(this.preparedStatementId, this.filterParams, this.updateParams); + } + + @Override + protected Result executePreparedStatement() { + StatementExecuteOk ok = this.mysqlxSession.sendMessage(getMessageBuilder().buildPrepareExecute(this.preparedStatementId, this.filterParams)); return new UpdateResult(ok); } public CompletableFuture executeAsync() { CompletableFuture okF = this.mysqlxSession - .asyncSendMessage(((XMessageBuilder) this.mysqlxSession. getMessageBuilder()).buildRowUpdate(this.filterParams, this.updateParams)); + .asyncSendMessage(((XMessageBuilder) this.mysqlxSession.getMessageBuilder()).buildRowUpdate(this.filterParams, this.updateParams)); return okF.thenApply(ok -> new UpdateResult(ok)); } public UpdateStatement set(Map fieldsAndValues) { + resetPrepareState(); this.updateParams.setUpdates(fieldsAndValues); return this; } public UpdateStatement set(String field, Object value) { + resetPrepareState(); this.updateParams.addUpdate(field, value); return this; } diff --git a/src/test/java/testsuite/simple/SequentialIdLeaseTest.java b/src/test/java/testsuite/simple/SequentialIdLeaseTest.java new file mode 100644 index 000000000..9c9cdd630 --- /dev/null +++ b/src/test/java/testsuite/simple/SequentialIdLeaseTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 2.0, as published by the + * Free Software Foundation. + * + * This program is also distributed with certain software (including but not + * limited to OpenSSL) that is licensed under separate terms, as designated in a + * particular file or component or in included license documentation. The + * authors of MySQL hereby grant you an additional permission to link the + * program and your derivative works with the separately licensed software that + * they have included with MySQL. + * + * Without limiting anything contained in the foregoing, this file, which is + * part of MySQL Connector/J, is also subject to the Universal FOSS Exception, + * version 1.0, a copy of which can be found at + * http://oss.oracle.com/licenses/universal-foss-exception. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, + * for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package testsuite.simple; + +import org.junit.Test; + +import com.mysql.cj.util.SequentialIdLease; + +import testsuite.BaseTestCase; + +public class SequentialIdLeaseTest extends BaseTestCase { + + public SequentialIdLeaseTest(String name) { + super(name); + } + + /** + * Runs all test cases in this test suite + * + * @param args + */ + public static void main(String[] args) { + junit.textui.TestRunner.run(StringUtilsTest.class); + } + + /** + * Tests the {@link SequentialIdLease} lease behavior. + */ + @Test + public void testSequentialIdLease() { + SequentialIdLease seqIdLease; + + // Release first. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(1); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + + // Release single id in middle. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(2); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + + // Release last. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(3); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + + // Release multiple in the beginning. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(1); + seqIdLease.releaseSequentialId(2); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(5, seqIdLease.allocateSequentialId()); + + // Release multiple in the middle. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(2); + seqIdLease.releaseSequentialId(3); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(5, seqIdLease.allocateSequentialId()); + + // Release multiple in the end. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(3); + seqIdLease.releaseSequentialId(4); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + assertEquals(5, seqIdLease.allocateSequentialId()); + + // Release interleaved. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(4, seqIdLease.allocateSequentialId()); + assertEquals(5, seqIdLease.allocateSequentialId()); + assertEquals(6, seqIdLease.allocateSequentialId()); + assertEquals(7, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(1); + seqIdLease.releaseSequentialId(3); + seqIdLease.releaseSequentialId(5); + seqIdLease.releaseSequentialId(7); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + assertEquals(5, seqIdLease.allocateSequentialId()); + assertEquals(7, seqIdLease.allocateSequentialId()); + assertEquals(8, seqIdLease.allocateSequentialId()); + + // Release all. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(1); + seqIdLease.releaseSequentialId(2); + seqIdLease.releaseSequentialId(3); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + + // Release non-existing. + seqIdLease = new SequentialIdLease(); + assertEquals(1, seqIdLease.allocateSequentialId()); + assertEquals(2, seqIdLease.allocateSequentialId()); + assertEquals(3, seqIdLease.allocateSequentialId()); + seqIdLease.releaseSequentialId(4); + assertEquals(4, seqIdLease.allocateSequentialId()); + + // Release from empty SequentialIdLease. + seqIdLease = new SequentialIdLease(); + seqIdLease.releaseSequentialId(1); + } +} diff --git a/src/test/java/testsuite/x/devapi/CollectionFindTest.java b/src/test/java/testsuite/x/devapi/CollectionFindTest.java index 6abadfcc9..d9192293c 100644 --- a/src/test/java/testsuite/x/devapi/CollectionFindTest.java +++ b/src/test/java/testsuite/x/devapi/CollectionFindTest.java @@ -59,6 +59,7 @@ import com.mysql.cj.xdevapi.Collection; import com.mysql.cj.xdevapi.DbDoc; import com.mysql.cj.xdevapi.DocResult; +import com.mysql.cj.xdevapi.FindStatement; import com.mysql.cj.xdevapi.JsonNumber; import com.mysql.cj.xdevapi.JsonString; import com.mysql.cj.xdevapi.Row; @@ -984,6 +985,221 @@ public void testBug21921956() { doc = res.fetchOne(); assertEquals("1004", ((JsonString) doc.get("_id")).getString()); } + } + + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + // Prepare test data. + this.collection.add("{\"_id\":\"1\", \"ord\": 1}", "{\"_id\":\"2\", \"ord\": 2}", "{\"_id\":\"3\", \"ord\": 3}", "{\"_id\":\"4\", \"ord\": 4}", + "{\"_id\":\"5\", \"ord\": 5}", "{\"_id\":\"6\", \"ord\": 6}", "{\"_id\":\"7\", \"ord\": 7}", "{\"_id\":\"8\", \"ord\": 8}").execute(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Collection testCol = testSession.getDefaultSchema().getCollection(this.collectionName); + + // Initialize several FindStatement objects. + FindStatement testFind1 = testCol.find(); // Find all. + FindStatement testFind2 = testCol.find("$.ord >= :n"); // Criteria with one placeholder. + FindStatement testFind3 = testCol.find("$.ord >= :n AND $.ord <= :n + 3"); // Criteria with same placeholder repeated. + FindStatement testFind4 = testCol.find("$.ord >= :n AND $.ord <= :m"); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testFind1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testFind2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testFind3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testFind4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind1, 0, -1); + assertTestPreparedStatementsResult(testFind2.bind("n", 2).execute(), 2, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind2, 0, -1); + assertTestPreparedStatementsResult(testFind3.bind("n", 2).execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testFind3, 0, -1); + assertTestPreparedStatementsResult(testFind4.bind("n", 2).bind("m", 5).execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testFind4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // B. Set sort resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testFind1.sort("$._id").execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind1, 0, -1); + assertTestPreparedStatementsResult(testFind2.sort("$._id").execute(), 2, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind2, 0, -1); + assertTestPreparedStatementsResult(testFind3.sort("$._id").execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testFind3, 0, -1); + assertTestPreparedStatementsResult(testFind4.sort("$._id").execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testFind4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 1); + assertTestPreparedStatementsResult(testFind2.bind("n", 3).execute(), 3, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testFind2, 2, 1); + assertTestPreparedStatementsResult(testFind3.bind("n", 3).execute(), 3, 6); + assertPreparedStatementsCountsAndId(testSession, 3, testFind3, 3, 1); + assertTestPreparedStatementsResult(testFind4.bind("m", 6).execute(), 2, 6); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 4, testFind1, 1, 2); + assertTestPreparedStatementsResult(testFind2.bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 4, testFind2, 2, 2); + assertTestPreparedStatementsResult(testFind3.bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testFind3, 3, 2); + assertTestPreparedStatementsResult(testFind4.bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + + // E. Set sort deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testFind1.sort("$._id").execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 3, testFind1, 0, -1); + assertTestPreparedStatementsResult(testFind2.sort("$._id").bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testFind2, 0, -1); + assertTestPreparedStatementsResult(testFind3.sort("$._id").bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 1, testFind3, 0, -1); + assertTestPreparedStatementsResult(testFind4.sort("$._id").bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 0, testFind4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 1); + assertTestPreparedStatementsResult(testFind2.bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testFind2, 2, 1); + assertTestPreparedStatementsResult(testFind3.bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 3, testFind3, 3, 1); + assertTestPreparedStatementsResult(testFind4.bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testFind1.limit(2).execute(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 4, testFind1, 1, 1); + assertTestPreparedStatementsResult(testFind2.limit(2).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testFind2, 2, 1); + assertTestPreparedStatementsResult(testFind3.limit(2).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testFind3, 3, 1); + assertTestPreparedStatementsResult(testFind4.limit(2).execute(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + + // H. Set limit and offset reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testFind1.limit(1).offset(1).execute(), 2, 2); + assertPreparedStatementsCountsAndId(testSession, 4, testFind1, 1, 2); + assertTestPreparedStatementsResult(testFind2.limit(1).offset(1).execute(), 5, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testFind2, 2, 2); + assertTestPreparedStatementsResult(testFind3.limit(1).offset(1).execute(), 5, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testFind3, 3, 2); + assertTestPreparedStatementsResult(testFind4.limit(1).offset(1).execute(), 4, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + + // I. Set sort deallocates and resets execution count, set limit and bind has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testFind1.sort("$._id").limit(2).execute(), 2, 3); + assertPreparedStatementsCountsAndId(testSession, 3, testFind1, 0, -1); + assertTestPreparedStatementsResult(testFind2.sort("$._id").limit(2).bind("n", 4).execute(), 5, 6); + assertPreparedStatementsCountsAndId(testSession, 2, testFind2, 0, -1); + assertTestPreparedStatementsResult(testFind3.sort("$._id").limit(2).bind("n", 4).execute(), 5, 6); + assertPreparedStatementsCountsAndId(testSession, 1, testFind3, 0, -1); + assertTestPreparedStatementsResult(testFind4.sort("$._id").limit(2).bind("n", 3).bind("m", 7).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testFind4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + + // J. Set offset reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testFind1.offset(0).execute(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 1); + assertTestPreparedStatementsResult(testFind2.offset(0).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 2, testFind2, 2, 1); + assertTestPreparedStatementsResult(testFind3.offset(0).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 3, testFind3, 3, 1); + assertTestPreparedStatementsResult(testFind4.offset(0).execute(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testFind4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testCol = testSession.getDefaultSchema().getCollection(this.collectionName); + + testFind1 = testCol.find(); + testFind2 = testCol.find(); + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind1, 0, -1); + assertTestPreparedStatementsResult(testFind2.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testFind2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 1); + assertTestPreparedStatementsResult(testFind2.execute(), 1, 8); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testFind2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testFind1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 2); + assertTestPreparedStatementsResult(testFind2.execute(), 1, 8); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testFind2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } + + private void assertTestPreparedStatementsResult(DocResult res, int expectedMin, int expectedMax) { + for (DbDoc d : res.fetchAll()) { + assertEquals(expectedMin++, ((JsonNumber) d.get("ord")).getInteger().intValue()); + } + assertEquals(expectedMax, expectedMin - 1); } } diff --git a/src/test/java/testsuite/x/devapi/CollectionModifyTest.java b/src/test/java/testsuite/x/devapi/CollectionModifyTest.java index cbf5fbcd8..348b95706 100644 --- a/src/test/java/testsuite/x/devapi/CollectionModifyTest.java +++ b/src/test/java/testsuite/x/devapi/CollectionModifyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -43,6 +43,7 @@ import com.mysql.cj.ServerVersion; import com.mysql.cj.xdevapi.AddResult; +import com.mysql.cj.xdevapi.Collection; import com.mysql.cj.xdevapi.DbDoc; import com.mysql.cj.xdevapi.DbDocImpl; import com.mysql.cj.xdevapi.DocResult; @@ -50,7 +51,10 @@ import com.mysql.cj.xdevapi.JsonNumber; import com.mysql.cj.xdevapi.JsonParser; import com.mysql.cj.xdevapi.JsonString; +import com.mysql.cj.xdevapi.ModifyStatement; import com.mysql.cj.xdevapi.Result; +import com.mysql.cj.xdevapi.Session; +import com.mysql.cj.xdevapi.SessionFactory; import com.mysql.cj.xdevapi.XDevAPIError; /** @@ -689,4 +693,251 @@ public void testBug27226293() { int age = ((JsonNumber) doc.get("age")).getInteger(); assertEquals(46, age); } + + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + testPreparedStatementsResetData(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Collection testCol1 = testSession.getDefaultSchema().getCollection(this.collectionName + "_1"); + Collection testCol2 = testSession.getDefaultSchema().getCollection(this.collectionName + "_2"); + Collection testCol3 = testSession.getDefaultSchema().getCollection(this.collectionName + "_3"); + Collection testCol4 = testSession.getDefaultSchema().getCollection(this.collectionName + "_4"); + + // Initialize several ModifyStatement objects. + ModifyStatement testModify1 = testCol1.modify("true").set("ord", expr("$.ord * 10")); // Modify all. + ModifyStatement testModify2 = testCol2.modify("$.ord >= :n").set("ord", expr("$.ord * 10")); // Criteria with one placeholder. + ModifyStatement testModify3 = testCol3.modify("$.ord >= :n AND $.ord <= :n + 1").set("ord", expr("$.ord * 10")); // Criteria with same placeholder repeated. + ModifyStatement testModify4 = testCol4.modify("$.ord >= :n AND $.ord <= :m").set("ord", expr("$.ord * 10")); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testModify1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testModify2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testModify3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testModify4, 0, -1); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify1, 0, -1); + assertTestPreparedStatementsResult(testModify2.bind("n", 2).execute(), 3, testCol2.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify2, 0, -1); + assertTestPreparedStatementsResult(testModify3.bind("n", 2).execute(), 2, testCol3.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify3, 0, -1); + assertTestPreparedStatementsResult(testModify4.bind("n", 2).bind("m", 3).execute(), 2, testCol4.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // B. Set sort resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testModify1.sort("$._id").execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify1, 0, -1); + assertTestPreparedStatementsResult(testModify2.sort("$._id").execute(), 3, testCol2.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify2, 0, -1); + assertTestPreparedStatementsResult(testModify3.sort("$._id").execute(), 2, testCol3.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify3, 0, -1); + assertTestPreparedStatementsResult(testModify4.sort("$._id").execute(), 2, testCol4.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testModify1, 1, 1); + assertTestPreparedStatementsResult(testModify2.bind("n", 3).execute(), 2, testCol2.getName(), 1, 2, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 2, testModify2, 2, 1); + assertTestPreparedStatementsResult(testModify3.bind("n", 3).execute(), 2, testCol3.getName(), 1, 2, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 3, testModify3, 3, 1); + assertTestPreparedStatementsResult(testModify4.bind("m", 4).execute(), 3, testCol4.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + testPreparedStatementsResetData(); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testModify1, 1, 2); + assertTestPreparedStatementsResult(testModify2.bind("n", 4).execute(), 1, testCol2.getName(), 1, 2, 3, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testModify2, 2, 2); + assertTestPreparedStatementsResult(testModify3.bind("n", 1).execute(), 2, testCol3.getName(), 10, 20, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify3, 3, 2); + assertTestPreparedStatementsResult(testModify4.bind("m", 2).execute(), 1, testCol4.getName(), 1, 20, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + testPreparedStatementsResetData(); + + // E. Set new values deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testModify1.set("ord", expr("$.ord * 100")).execute(), 4, testCol1.getName(), 100, 200, 300, 400); + assertPreparedStatementsCountsAndId(testSession, 3, testModify1, 0, -1); + assertTestPreparedStatementsResult(testModify2.set("ord", expr("$.ord * 100")).execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testModify2, 0, -1); + assertTestPreparedStatementsResult(testModify3.set("ord", expr("$.ord * 100")).execute(), 2, testCol3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testModify3, 0, -1); + assertTestPreparedStatementsResult(testModify4.set("ord", expr("$.ord * 100")).execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + testPreparedStatementsResetData(); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 100, 200, 300, 400); + assertPreparedStatementsCountsAndId(testSession, 1, testModify1, 1, 1); + assertTestPreparedStatementsResult(testModify2.execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testModify2, 2, 1); + assertTestPreparedStatementsResult(testModify3.execute(), 2, testCol3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testModify3, 3, 1); + assertTestPreparedStatementsResult(testModify4.execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + testPreparedStatementsResetData(); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testModify1.limit(1).execute(), 1, testCol1.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify1, 1, 1); + assertTestPreparedStatementsResult(testModify2.limit(1).execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 4, testModify2, 2, 1); + assertTestPreparedStatementsResult(testModify3.limit(1).execute(), 1, testCol3.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify3, 3, 1); + assertTestPreparedStatementsResult(testModify4.limit(1).execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + testPreparedStatementsResetData(); + + // H. Set limit reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testModify1.limit(2).execute(), 2, testCol1.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify1, 1, 2); + assertTestPreparedStatementsResult(testModify2.limit(2).execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 4, testModify2, 2, 2); + assertTestPreparedStatementsResult(testModify3.limit(2).execute(), 2, testCol3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify3, 3, 2); + assertTestPreparedStatementsResult(testModify4.limit(2).execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + testPreparedStatementsResetData(); + + // I. Set sort deallocates and resets execution count, set limit has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testModify1.sort("$._id").limit(1).execute(), 1, testCol1.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testModify1, 0, -1); + assertTestPreparedStatementsResult(testModify2.sort("$._id").limit(1).execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testModify2, 0, -1); + assertTestPreparedStatementsResult(testModify3.sort("$._id").limit(1).execute(), 1, testCol3.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testModify3, 0, -1); + assertTestPreparedStatementsResult(testModify4.sort("$._id").limit(1).execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testModify4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + testPreparedStatementsResetData(); + + // J. Set limit reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testModify1.limit(2).execute(), 2, testCol1.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testModify1, 1, 1); + assertTestPreparedStatementsResult(testModify2.limit(2).execute(), 1, testCol2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testModify2, 2, 1); + assertTestPreparedStatementsResult(testModify3.limit(2).execute(), 2, testCol3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testModify3, 3, 1); + assertTestPreparedStatementsResult(testModify4.limit(2).execute(), 1, testCol4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testModify4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testCol1 = testSession.getDefaultSchema().getCollection(this.collectionName + "_1"); + testCol2 = testSession.getDefaultSchema().getCollection(this.collectionName + "_2"); + + testModify1 = testCol1.modify("true").set("ord", expr("$.ord * 10")); + testModify2 = testCol2.modify("true").set("ord", expr("$.ord * 10")); + + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify1, 0, -1); + assertTestPreparedStatementsResult(testModify2.execute(), 4, testCol2.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testModify2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testModify1, 1, 1); + assertTestPreparedStatementsResult(testModify2.execute(), 4, testCol2.getName(), 10, 20, 30, 40); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testModify2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + testPreparedStatementsResetData(); + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testModify1.execute(), 4, testCol1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testModify1, 1, 2); + assertTestPreparedStatementsResult(testModify2.execute(), 4, testCol2.getName(), 10, 20, 30, 40); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testModify2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } finally { + for (int i = 0; i < 4; i++) { + dropCollection(this.collectionName + "_" + (i + 1)); + } + } + } + + private void testPreparedStatementsResetData() { + for (int i = 0; i < 4; i++) { + Collection col = this.session.getDefaultSchema().createCollection(this.collectionName + "_" + (i + 1), true); + col.remove("true").execute(); + col.add("{\"_id\":\"1\", \"ord\": 1}", "{\"_id\":\"2\", \"ord\": 2}", "{\"_id\":\"3\", \"ord\": 3}", "{\"_id\":\"4\", \"ord\": 4}").execute(); + } + } + + @SuppressWarnings("hiding") + private void assertTestPreparedStatementsResult(Result res, int expectedAffectedItemsCount, String collectionName, int... expectedValues) { + assertEquals(expectedAffectedItemsCount, res.getAffectedItemsCount()); + DocResult docRes = this.schema.getCollection(collectionName).find().execute(); + assertEquals(expectedValues.length, docRes.count()); + for (int v : expectedValues) { + assertEquals(v, ((JsonNumber) docRes.next().get("ord")).getInteger().intValue()); + } + } } diff --git a/src/test/java/testsuite/x/devapi/CollectionRemoveTest.java b/src/test/java/testsuite/x/devapi/CollectionRemoveTest.java index 101d3c644..d537f73bc 100644 --- a/src/test/java/testsuite/x/devapi/CollectionRemoveTest.java +++ b/src/test/java/testsuite/x/devapi/CollectionRemoveTest.java @@ -38,7 +38,13 @@ import org.junit.Test; import com.mysql.cj.ServerVersion; +import com.mysql.cj.xdevapi.Collection; +import com.mysql.cj.xdevapi.DocResult; +import com.mysql.cj.xdevapi.JsonNumber; +import com.mysql.cj.xdevapi.RemoveStatement; import com.mysql.cj.xdevapi.Result; +import com.mysql.cj.xdevapi.Session; +import com.mysql.cj.xdevapi.SessionFactory; import com.mysql.cj.xdevapi.XDevAPIError; /** @@ -145,4 +151,251 @@ public void removeOne() { assertEquals(2, this.collection.count()); assertFalse(this.collection.find("x = 3").execute().hasNext()); } + + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + testPreparedStatementsResetData(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Collection testCol1 = testSession.getDefaultSchema().getCollection(this.collectionName + "_1"); + Collection testCol2 = testSession.getDefaultSchema().getCollection(this.collectionName + "_2"); + Collection testCol3 = testSession.getDefaultSchema().getCollection(this.collectionName + "_3"); + Collection testCol4 = testSession.getDefaultSchema().getCollection(this.collectionName + "_4"); + + // Initialize several RemoveStatement objects. + RemoveStatement testRemove1 = testCol1.remove("true"); // Remove all. + RemoveStatement testRemove2 = testCol2.remove("$.ord >= :n"); // Criteria with one placeholder. + RemoveStatement testRemove3 = testCol3.remove("$.ord >= :n AND $.ord <= :n + 1"); // Criteria with same placeholder repeated. + RemoveStatement testRemove4 = testCol4.remove("$.ord >= :n AND $.ord <= :m"); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testRemove1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove4, 0, -1); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove1, 0, -1); + assertTestPreparedStatementsResult(testRemove2.bind("n", 2).execute(), 3, testCol2.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove2, 0, -1); + assertTestPreparedStatementsResult(testRemove3.bind("n", 2).execute(), 2, testCol3.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove3, 0, -1); + assertTestPreparedStatementsResult(testRemove4.bind("n", 2).bind("m", 3).execute(), 2, testCol4.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // B. Set sort resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testRemove1.sort("$._id").execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove1, 0, -1); + assertTestPreparedStatementsResult(testRemove2.sort("$._id").execute(), 3, testCol2.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove2, 0, -1); + assertTestPreparedStatementsResult(testRemove3.sort("$._id").execute(), 2, testCol3.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove3, 0, -1); + assertTestPreparedStatementsResult(testRemove4.sort("$._id").execute(), 2, testCol4.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove1, 1, 1); + assertTestPreparedStatementsResult(testRemove2.bind("n", 3).execute(), 2, testCol2.getName(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 2, testRemove2, 2, 1); + assertTestPreparedStatementsResult(testRemove3.bind("n", 3).execute(), 2, testCol3.getName(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 3, testRemove3, 3, 1); + assertTestPreparedStatementsResult(testRemove4.bind("m", 4).execute(), 3, testCol4.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + testPreparedStatementsResetData(); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove1, 1, 2); + assertTestPreparedStatementsResult(testRemove2.bind("n", 4).execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove2, 2, 2); + assertTestPreparedStatementsResult(testRemove3.bind("n", 1).execute(), 2, testCol3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove3, 3, 2); + assertTestPreparedStatementsResult(testRemove4.bind("m", 2).execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + testPreparedStatementsResetData(); + + // E. Set sort deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testRemove1.sort("$._id").execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 3, testRemove1, 0, -1); + assertTestPreparedStatementsResult(testRemove2.sort("$._id").execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testRemove2, 0, -1); + assertTestPreparedStatementsResult(testRemove3.sort("$._id").execute(), 2, testCol3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove3, 0, -1); + assertTestPreparedStatementsResult(testRemove4.sort("$._id").execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + testPreparedStatementsResetData(); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove1, 1, 1); + assertTestPreparedStatementsResult(testRemove2.execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testRemove2, 2, 1); + assertTestPreparedStatementsResult(testRemove3.execute(), 2, testCol3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testRemove3, 3, 1); + assertTestPreparedStatementsResult(testRemove4.execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + testPreparedStatementsResetData(); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testRemove1.limit(1).execute(), 1, testCol1.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove1, 1, 1); + assertTestPreparedStatementsResult(testRemove2.limit(1).execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove2, 2, 1); + assertTestPreparedStatementsResult(testRemove3.limit(1).execute(), 1, testCol3.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove3, 3, 1); + assertTestPreparedStatementsResult(testRemove4.limit(1).execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + testPreparedStatementsResetData(); + + // H. Set limit reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testRemove1.limit(2).execute(), 2, testCol1.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove1, 1, 2); + assertTestPreparedStatementsResult(testRemove2.limit(2).execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove2, 2, 2); + assertTestPreparedStatementsResult(testRemove3.limit(2).execute(), 2, testCol3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove3, 3, 2); + assertTestPreparedStatementsResult(testRemove4.limit(2).execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + testPreparedStatementsResetData(); + + // I. Set sort deallocates and resets execution count, set limit has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testRemove1.sort("$._id").limit(1).execute(), 1, testCol1.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testRemove1, 0, -1); + assertTestPreparedStatementsResult(testRemove2.sort("$._id").limit(1).execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testRemove2, 0, -1); + assertTestPreparedStatementsResult(testRemove3.sort("$._id").limit(1).execute(), 1, testCol3.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove3, 0, -1); + assertTestPreparedStatementsResult(testRemove4.sort("$._id").limit(1).execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + testPreparedStatementsResetData(); + + // J. Set limit reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testRemove1.limit(2).execute(), 2, testCol1.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove1, 1, 1); + assertTestPreparedStatementsResult(testRemove2.limit(2).execute(), 1, testCol2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testRemove2, 2, 1); + assertTestPreparedStatementsResult(testRemove3.limit(2).execute(), 2, testCol3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testRemove3, 3, 1); + assertTestPreparedStatementsResult(testRemove4.limit(2).execute(), 1, testCol4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testRemove4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testCol1 = testSession.getDefaultSchema().getCollection(this.collectionName + "_1"); + testCol2 = testSession.getDefaultSchema().getCollection(this.collectionName + "_2"); + + testRemove1 = testCol1.remove("true"); + testRemove2 = testCol2.remove("true"); + + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove1, 0, -1); + assertTestPreparedStatementsResult(testRemove2.execute(), 4, testCol2.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testRemove2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove1, 1, 1); + assertTestPreparedStatementsResult(testRemove2.execute(), 4, testCol2.getName()); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testRemove2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + testPreparedStatementsResetData(); + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testRemove1.execute(), 4, testCol1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testRemove1, 1, 2); + assertTestPreparedStatementsResult(testRemove2.execute(), 4, testCol2.getName()); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testRemove2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } finally { + for (int i = 0; i < 4; i++) { + dropCollection(this.collectionName + "_" + (i + 1)); + } + } + } + + private void testPreparedStatementsResetData() { + for (int i = 0; i < 4; i++) { + Collection col = this.session.getDefaultSchema().createCollection(this.collectionName + "_" + (i + 1), true); + col.remove("true").execute(); + col.add("{\"_id\":\"1\", \"ord\": 1}", "{\"_id\":\"2\", \"ord\": 2}", "{\"_id\":\"3\", \"ord\": 3}", "{\"_id\":\"4\", \"ord\": 4}").execute(); + } + } + + @SuppressWarnings("hiding") + private void assertTestPreparedStatementsResult(Result res, int expectedAffectedItemsCount, String collectionName, int... expectedValues) { + assertEquals(expectedAffectedItemsCount, res.getAffectedItemsCount()); + DocResult docRes = this.schema.getCollection(collectionName).find().execute(); + assertEquals(expectedValues.length, docRes.count()); + for (int v : expectedValues) { + assertEquals(v, ((JsonNumber) docRes.next().get("ord")).getInteger().intValue()); + } + } } diff --git a/src/test/java/testsuite/x/devapi/DevApiBaseTestCase.java b/src/test/java/testsuite/x/devapi/DevApiBaseTestCase.java index f7af35a69..e06ee9a1e 100644 --- a/src/test/java/testsuite/x/devapi/DevApiBaseTestCase.java +++ b/src/test/java/testsuite/x/devapi/DevApiBaseTestCase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -29,6 +29,8 @@ package testsuite.x.devapi; +import static org.junit.Assert.assertEquals; + import java.lang.reflect.Field; import java.sql.SQLException; @@ -36,10 +38,12 @@ import com.mysql.cj.conf.PropertyKey; import com.mysql.cj.exceptions.MysqlErrorNumbers; import com.mysql.cj.protocol.x.XProtocolError; +import com.mysql.cj.xdevapi.PreparableStatement; import com.mysql.cj.xdevapi.Schema; import com.mysql.cj.xdevapi.Session; import com.mysql.cj.xdevapi.SessionImpl; import com.mysql.cj.xdevapi.SqlResult; +import com.mysql.cj.xdevapi.Statement; import testsuite.x.internal.InternalXBaseTestCase; @@ -120,4 +124,82 @@ protected boolean isServerRunningOnWindows() throws SQLException { SqlResult res = this.session.sql("SHOW VARIABLES LIKE 'datadir'").execute(); return res.fetchOne().getString(1).indexOf('\\') != -1; } + + protected int getThreadId(Session sess) { + return sess.sql("SELECT thread_id FROM performance_schema.threads WHERE processlist_id = connection_id()").execute().fetchOne().getInt(0); + } + + int getPrepPrepareCount(Session sess) { + return Integer.parseInt(sess.sql("SHOW SESSION STATUS LIKE 'mysqlx_prep_prepare'").execute().fetchOne().getString(1)); + } + + int getPrepExecuteCount(Session sess) { + return Integer.parseInt(sess.sql("SHOW SESSION STATUS LIKE 'mysqlx_prep_execute'").execute().fetchOne().getString(1)); + } + + int getPrepDeallocateCount(Session sess) { + return Integer.parseInt(sess.sql("SHOW SESSION STATUS LIKE 'mysqlx_prep_deallocate'").execute().fetchOne().getString(1)); + } + + int getPreparedStatementsCount() { + return this.session.sql("SELECT COUNT(*) FROM performance_schema.prepared_statements_instances").execute().fetchOne().getInt(0); + } + + int getPreparedStatementsCount(int threadId) { + return this.session.sql("SELECT COUNT(*) FROM performance_schema.prepared_statements_instances WHERE owner_thread_id = " + threadId).execute() + .fetchOne().getInt(0); + } + + int getPreparedStatementsCount(Session sess) { + return sess.sql("SELECT COUNT(*) FROM performance_schema.prepared_statements_instances psi INNER JOIN performance_schema.threads t " + + "ON psi.owner_thread_id = t.thread_id WHERE t.processlist_id = connection_id()").execute().fetchOne().getInt(0); + } + + int getPreparedStatementExecutionsCount(Session sess, int prepStmtId) { + SqlResult res = sess.sql("SELECT psi.count_execute FROM performance_schema.prepared_statements_instances psi INNER JOIN performance_schema.threads t " + + "ON psi.owner_thread_id = t.thread_id WHERE t.processlist_id = connection_id() AND psi.statement_id = mysqlx_get_prepared_statement_id(?)") + .bind(prepStmtId).execute(); + if (res.hasNext()) { + return res.next().getInt(0); + } + return -1; + } + + int getPreparedStatementId(PreparableStatement stmt) { + try { + Field prepStmtId = PreparableStatement.class.getDeclaredField("preparedStatementId"); + prepStmtId.setAccessible(true); + return prepStmtId.getInt(stmt); + } catch (Exception e) { + return -1; + } + } + + protected void assertPreparedStatementsCountsAndId(Session sess, int expectedPrepStmtsCount, Statement stmt, int expectedId, int expectedExec) { + assertEquals(expectedPrepStmtsCount, getPreparedStatementsCount(sess)); + assertEquals(expectedId, getPreparedStatementId((PreparableStatement) stmt)); + assertEquals(expectedExec, getPreparedStatementExecutionsCount(sess, expectedId)); + } + + protected void assertPreparedStatementsStatusCounts(Session sess, int expectedPrep, int expectedExec, int expectedDealloc) { + assertEquals(expectedPrep, getPrepPrepareCount(sess)); + assertEquals(expectedExec, getPrepExecuteCount(sess)); + assertEquals(expectedDealloc, getPrepDeallocateCount(sess)); + } + + protected void assertPreparedStatementsCount(int threadId, int expectedCount, int countdown) { + /* + * System table performance_schema.prepared_statements_instances may have some delay in updating its values after a session containing + * prepared statements is closed. + */ + int psCount; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + psCount = getPreparedStatementsCount(threadId); + } while (psCount != 0 && --countdown > 0); + assertEquals(expectedCount, psCount); + } } diff --git a/src/test/java/testsuite/x/devapi/SessionTest.java b/src/test/java/testsuite/x/devapi/SessionTest.java index 2ae706574..a9f419554 100644 --- a/src/test/java/testsuite/x/devapi/SessionTest.java +++ b/src/test/java/testsuite/x/devapi/SessionTest.java @@ -69,9 +69,11 @@ import com.mysql.cj.xdevapi.ClientFactory; import com.mysql.cj.xdevapi.ClientImpl; import com.mysql.cj.xdevapi.ClientImpl.PooledXProtocol; +import com.mysql.cj.xdevapi.FindStatement; import com.mysql.cj.xdevapi.Row; import com.mysql.cj.xdevapi.RowResult; import com.mysql.cj.xdevapi.Schema; +import com.mysql.cj.xdevapi.SelectStatement; import com.mysql.cj.xdevapi.Session; import com.mysql.cj.xdevapi.SessionFactory; import com.mysql.cj.xdevapi.SessionImpl; @@ -81,12 +83,12 @@ public class SessionTest extends DevApiBaseTestCase { @Before - public void setupCollectionTest() { + public void setupSessionTest() { setupTestSession(); } @After - public void teardownCollectionTest() { + public void teardownSessionTest() { if (this.isSetForXTests) { this.createdTestSchemas.forEach(schemaName -> { try { @@ -1136,9 +1138,7 @@ public void testBug28606708() throws Exception { } throw e; } - } - } @Test @@ -1369,4 +1369,196 @@ private void testSessionAttributes_checkClient(String url, Map u c.close(); } } + + @Test + public void testPreparedStatementsCleanup() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + this.schema.createCollection("testPrepStmtClean", true).add("{\"_id\":\"1\"}").execute(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // Initialize several *Statement objects. + FindStatement testFind1 = testSession.getDefaultSchema().getCollection("testPrepStmtClean").find(); + SelectStatement testSelect1 = testSession.getDefaultSchema().getCollectionAsTable("testPrepStmtClean").select("_id"); + FindStatement testFind2 = testSession.getDefaultSchema().getCollection("testPrepStmtClean").find(); + SelectStatement testSelect2 = testSession.getDefaultSchema().getCollectionAsTable("testPrepStmtClean").select("_id"); + + // 1st execute -> don't prepare. + testFind1.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testFind1, 0, -1); + testSelect1.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect1, 0, -1); + testFind2.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testFind2, 0, -1); + testSelect2.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // 2nd execute -> prepare + execute. + testFind1.execute(); + assertPreparedStatementsCountsAndId(testSession, 1, testFind1, 1, 1); + testSelect1.execute(); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect1, 2, 1); + testFind2.execute(); + assertPreparedStatementsCountsAndId(testSession, 3, testFind2, 3, 1); + testSelect2.execute(); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect2, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + assertPreparedStatementsCount(sessionThreadId, 4, 1); + + /* + * The following verifications are non-deterministic as System.gc() only hints the JVM to perform a garbage collection. This approach allows some + * time for the JVM to execute the GC. In case of failure the repeats or wait times may have to be adjusted. + * The test can be deleted entirely if no reasonable setup can be found. + */ + + // Nullify first statement. + testFind1 = null; + System.gc(); + int psCount, countdown = 10; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + testSession.sql("SELECT 1").execute(); + psCount = getPreparedStatementsCount(sessionThreadId); + } while (psCount != 3 && --countdown > 0); + assertPreparedStatementsStatusCounts(testSession, 4, 4, 1); + assertPreparedStatementsCount(sessionThreadId, 3, 1); + + // Nullify second and third statements. + testSelect1 = null; + testFind2 = null; + System.gc(); + countdown = 10; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + testSession.sql("SELECT 1").execute(); + psCount = getPreparedStatementsCount(sessionThreadId); + } while (psCount != 1 && --countdown > 0); + assertPreparedStatementsStatusCounts(testSession, 4, 4, 3); + assertPreparedStatementsCount(sessionThreadId, 1, 1); + + // Nullify last statement. + testSelect2 = null; + System.gc(); + countdown = 10; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + testSession.sql("SELECT 1").execute(); + psCount = getPreparedStatementsCount(sessionThreadId); + } while (psCount != 0 && --countdown > 0); + assertPreparedStatementsStatusCounts(testSession, 4, 4, 4); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + } finally { + this.schema.dropCollection("testPrepStmtClean"); + } + } + + @Test + public void testPreparedStatementsPooledConnections() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + Properties props = new Properties(); + props.setProperty(ClientProperty.POOLING_ENABLED.getKeyName(), "true"); + props.setProperty(ClientProperty.POOLING_MAX_SIZE.getKeyName(), "1"); + + try { + this.schema.createCollection("testPrepStmtPooling", true).add("{\"_id\":\"1\"}").execute(); + + ClientFactory cf = new ClientFactory(); + Client testClient = cf.getClient(this.baseUrl, props); + + Session testSession = testClient.getSession(); + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + + FindStatement testFind = testSession.getDefaultSchema().getCollection("testPrepStmtPooling").find(); + + // 1st execute -> don't prepare. + testFind.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testFind, 0, -1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // 2nd execute -> prepare + execute. + testFind.execute(); + assertPreparedStatementsCountsAndId(testSession, 1, testFind, 1, 1); + assertPreparedStatementsStatusCounts(testSession, 1, 1, 0); + + assertPreparedStatementsCount(sessionThreadId, 1, 1); + + // Prepared statements won't live past closing the session, or returning it to the pool. + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); + + testSession = testClient.getSession(); + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + + // The underlying connection object in testFind is the same as the one returned from the pool to the new session. + assertThrows(XProtocolError.class, "ERROR 5110 \\(HY000\\) Statement with ID=1 was not prepared", testFind::execute); // This exec attempt counts. + if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.16"))) { // Mysqlx.Session.Reset doesn't clear PS counters. + assertPreparedStatementsStatusCounts(testSession, 1, 2, 0); + } else { + assertPreparedStatementsStatusCounts(testSession, 0, 1, 0); + } + + testFind = testSession.getDefaultSchema().getCollection("testPrepStmtPooling").find(); + + // 1st execute -> don't prepare. + testFind.execute(); + assertPreparedStatementsCountsAndId(testSession, 0, testFind, 0, -1); + if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.16"))) { // Mysqlx.Session.Reset doesn't clear PS counters. + assertPreparedStatementsStatusCounts(testSession, 1, 2, 0); + } else { + assertPreparedStatementsStatusCounts(testSession, 0, 1, 0); + } + // 2nd execute -> prepare + execute. + testFind.execute(); + assertPreparedStatementsCountsAndId(testSession, 1, testFind, 1, 1); + if (mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.16"))) { // Mysqlx.Session.Reset doesn't clear PS counters. + assertPreparedStatementsStatusCounts(testSession, 2, 3, 0); + } else { + assertPreparedStatementsStatusCounts(testSession, 1, 2, 0); + } + + assertPreparedStatementsCount(sessionThreadId, 1, 1); + + // Prepared statements won't live past closing the client and its sessions. + testClient.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); + + assertThrows(CJCommunicationsException.class, "Unable to write message", testFind::execute); + } finally { + this.schema.dropCollection("testPrepStmtPooling"); + } + } } diff --git a/src/test/java/testsuite/x/devapi/TableDeleteTest.java b/src/test/java/testsuite/x/devapi/TableDeleteTest.java index 111743056..a7e26f6ec 100644 --- a/src/test/java/testsuite/x/devapi/TableDeleteTest.java +++ b/src/test/java/testsuite/x/devapi/TableDeleteTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -33,6 +33,12 @@ import org.junit.Test; +import com.mysql.cj.ServerVersion; +import com.mysql.cj.xdevapi.DeleteStatement; +import com.mysql.cj.xdevapi.Result; +import com.mysql.cj.xdevapi.RowResult; +import com.mysql.cj.xdevapi.Session; +import com.mysql.cj.xdevapi.SessionFactory; import com.mysql.cj.xdevapi.Table; /** @@ -73,5 +79,249 @@ public void testDelete() { } } - // TODO: there could be more tests, incl limit? + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + testPreparedStatementsResetData(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Table testTbl1 = testSession.getDefaultSchema().getTable("testPrepareDelete_1"); + Table testTbl2 = testSession.getDefaultSchema().getTable("testPrepareDelete_2"); + Table testTbl3 = testSession.getDefaultSchema().getTable("testPrepareDelete_3"); + Table testTbl4 = testSession.getDefaultSchema().getTable("testPrepareDelete_4"); + + // Initialize several DeleteStatement objects. + DeleteStatement testDelete1 = testTbl1.delete().where("true"); // Delete all. + DeleteStatement testDelete2 = testTbl2.delete().where("ord >= :n"); // Criteria with one placeholder. + DeleteStatement testDelete3 = testTbl3.delete().where("ord >= :n AND ord <= :n + 1"); // Criteria with same placeholder repeated. + DeleteStatement testDelete4 = testTbl4.delete().where("ord >= :n AND ord <= :m"); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testDelete1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete4, 0, -1); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete1, 0, -1); + assertTestPreparedStatementsResult(testDelete2.bind("n", 2).execute(), 3, testTbl2.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete2, 0, -1); + assertTestPreparedStatementsResult(testDelete3.bind("n", 2).execute(), 2, testTbl3.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete3, 0, -1); + assertTestPreparedStatementsResult(testDelete4.bind("n", 2).bind("m", 3).execute(), 2, testTbl4.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // B. Set orderBy resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testDelete1.orderBy("id").execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete1, 0, -1); + assertTestPreparedStatementsResult(testDelete2.orderBy("id").execute(), 3, testTbl2.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete2, 0, -1); + assertTestPreparedStatementsResult(testDelete3.orderBy("id").execute(), 2, testTbl3.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete3, 0, -1); + assertTestPreparedStatementsResult(testDelete4.orderBy("id").execute(), 2, testTbl4.getName(), 1, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete1, 1, 1); + assertTestPreparedStatementsResult(testDelete2.bind("n", 3).execute(), 2, testTbl2.getName(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 2, testDelete2, 2, 1); + assertTestPreparedStatementsResult(testDelete3.bind("n", 3).execute(), 2, testTbl3.getName(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 3, testDelete3, 3, 1); + assertTestPreparedStatementsResult(testDelete4.bind("m", 4).execute(), 3, testTbl4.getName(), 1); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + testPreparedStatementsResetData(); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete1, 1, 2); + assertTestPreparedStatementsResult(testDelete2.bind("n", 4).execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete2, 2, 2); + assertTestPreparedStatementsResult(testDelete3.bind("n", 1).execute(), 2, testTbl3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete3, 3, 2); + assertTestPreparedStatementsResult(testDelete4.bind("m", 2).execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + testPreparedStatementsResetData(); + + // E. Set orderBy deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testDelete1.orderBy("id").execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 3, testDelete1, 0, -1); + assertTestPreparedStatementsResult(testDelete2.orderBy("id").execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testDelete2, 0, -1); + assertTestPreparedStatementsResult(testDelete3.orderBy("id").execute(), 2, testTbl3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete3, 0, -1); + assertTestPreparedStatementsResult(testDelete4.orderBy("id").execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + testPreparedStatementsResetData(); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete1, 1, 1); + assertTestPreparedStatementsResult(testDelete2.execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testDelete2, 2, 1); + assertTestPreparedStatementsResult(testDelete3.execute(), 2, testTbl3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testDelete3, 3, 1); + assertTestPreparedStatementsResult(testDelete4.execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + testPreparedStatementsResetData(); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testDelete1.limit(1).execute(), 1, testTbl1.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete1, 1, 1); + assertTestPreparedStatementsResult(testDelete2.limit(1).execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete2, 2, 1); + assertTestPreparedStatementsResult(testDelete3.limit(1).execute(), 1, testTbl3.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete3, 3, 1); + assertTestPreparedStatementsResult(testDelete4.limit(1).execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + testPreparedStatementsResetData(); + + // H. Set limit reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testDelete1.limit(2).execute(), 2, testTbl1.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete1, 1, 2); + assertTestPreparedStatementsResult(testDelete2.limit(2).execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete2, 2, 2); + assertTestPreparedStatementsResult(testDelete3.limit(2).execute(), 2, testTbl3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete3, 3, 2); + assertTestPreparedStatementsResult(testDelete4.limit(2).execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + testPreparedStatementsResetData(); + + // I. Set sort deallocates and resets execution count, set limit has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testDelete1.orderBy("id").limit(1).execute(), 1, testTbl1.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testDelete1, 0, -1); + assertTestPreparedStatementsResult(testDelete2.orderBy("id").limit(1).execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testDelete2, 0, -1); + assertTestPreparedStatementsResult(testDelete3.orderBy("id").limit(1).execute(), 1, testTbl3.getName(), 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete3, 0, -1); + assertTestPreparedStatementsResult(testDelete4.orderBy("id").limit(1).execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + testPreparedStatementsResetData(); + + // J. Set limit reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testDelete1.limit(2).execute(), 2, testTbl1.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete1, 1, 1); + assertTestPreparedStatementsResult(testDelete2.limit(2).execute(), 1, testTbl2.getName(), 1, 2, 3); + assertPreparedStatementsCountsAndId(testSession, 2, testDelete2, 2, 1); + assertTestPreparedStatementsResult(testDelete3.limit(2).execute(), 2, testTbl3.getName(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testDelete3, 3, 1); + assertTestPreparedStatementsResult(testDelete4.limit(2).execute(), 1, testTbl4.getName(), 1, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testDelete4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testTbl1 = testSession.getDefaultSchema().getTable("testPrepareDelete_1"); + testTbl2 = testSession.getDefaultSchema().getTable("testPrepareDelete_2"); + + testDelete1 = testTbl1.delete().where("true"); + testDelete2 = testTbl2.delete().where("true"); + + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete1, 0, -1); + assertTestPreparedStatementsResult(testDelete2.execute(), 4, testTbl2.getName()); + assertPreparedStatementsCountsAndId(testSession, 0, testDelete2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete1, 1, 1); + assertTestPreparedStatementsResult(testDelete2.execute(), 4, testTbl2.getName()); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testDelete2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + testPreparedStatementsResetData(); + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testDelete1.execute(), 4, testTbl1.getName()); + assertPreparedStatementsCountsAndId(testSession, 1, testDelete1, 1, 2); + assertTestPreparedStatementsResult(testDelete2.execute(), 4, testTbl2.getName()); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testDelete2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } finally { + for (int i = 0; i < 4; i++) { + sqlUpdate("DROP TABLE IF EXISTS testPrepareDelete_" + (i + 1)); + } + } + } + + private void testPreparedStatementsResetData() { + for (int i = 0; i < 4; i++) { + sqlUpdate("CREATE TABLE IF NOT EXISTS testPrepareDelete_" + (i + 1) + " (id INT PRIMARY KEY, ord INT)"); + sqlUpdate("TRUNCATE TABLE testPrepareDelete_" + (i + 1)); + sqlUpdate("INSERT INTO testPrepareDelete_" + (i + 1) + " VALUES (1, 1), (2, 2), (3, 3), (4, 4)"); + } + } + + private void assertTestPreparedStatementsResult(Result res, int expectedAffectedItemsCount, String tableName, int... expectedValues) { + assertEquals(expectedAffectedItemsCount, res.getAffectedItemsCount()); + RowResult rowRes = this.schema.getTable(tableName).select("ord").execute(); + assertEquals(expectedValues.length, rowRes.count()); + for (int v : expectedValues) { + assertEquals(v, rowRes.next().getInt("ord")); + } + } } diff --git a/src/test/java/testsuite/x/devapi/TableSelectTest.java b/src/test/java/testsuite/x/devapi/TableSelectTest.java index 5244b30fb..a88295b34 100644 --- a/src/test/java/testsuite/x/devapi/TableSelectTest.java +++ b/src/test/java/testsuite/x/devapi/TableSelectTest.java @@ -946,4 +946,224 @@ public void testBug22038729() throws Exception { sqlUpdate("drop procedure if exists testBug22038729p"); } } + + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + sqlUpdate("DROP TABLE IF EXISTS testPrepareSelect"); + sqlUpdate("CREATE TABLE testPrepareSelect (id INT PRIMARY KEY, ord INT)"); + sqlUpdate("INSERT INTO testPrepareSelect VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8)"); + + SessionFactory sf = new SessionFactory(); + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Table testTbl = testSession.getDefaultSchema().getTable("testPrepareSelect"); + + // Initialize several SelectStatement objects. + SelectStatement testSelect1 = testTbl.select("ord"); // Select all. + SelectStatement testSelect2 = testTbl.select("ord").where("ord >= :n"); // Criteria with one placeholder. + SelectStatement testSelect3 = testTbl.select("ord").where("ord >= :n AND ord <= :n + 3"); // Criteria with same placeholder repeated. + SelectStatement testSelect4 = testTbl.select("ord").where("ord >= :n AND ord <= :m"); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testSelect1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect1, 0, -1); + assertTestPreparedStatementsResult(testSelect2.bind("n", 2).execute(), 2, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect2, 0, -1); + assertTestPreparedStatementsResult(testSelect3.bind("n", 2).execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect3, 0, -1); + assertTestPreparedStatementsResult(testSelect4.bind("n", 2).bind("m", 5).execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // B. Set orderBy resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testSelect1.orderBy("id").execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect1, 0, -1); + assertTestPreparedStatementsResult(testSelect2.orderBy("id").execute(), 2, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect2, 0, -1); + assertTestPreparedStatementsResult(testSelect3.orderBy("id").execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect3, 0, -1); + assertTestPreparedStatementsResult(testSelect4.orderBy("id").execute(), 2, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect1, 1, 1); + assertTestPreparedStatementsResult(testSelect2.bind("n", 3).execute(), 3, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect2, 2, 1); + assertTestPreparedStatementsResult(testSelect3.bind("n", 3).execute(), 3, 6); + assertPreparedStatementsCountsAndId(testSession, 3, testSelect3, 3, 1); + assertTestPreparedStatementsResult(testSelect4.bind("m", 6).execute(), 2, 6); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect1, 1, 2); + assertTestPreparedStatementsResult(testSelect2.bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect2, 2, 2); + assertTestPreparedStatementsResult(testSelect3.bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect3, 3, 2); + assertTestPreparedStatementsResult(testSelect4.bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + + // E. Set where deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testSelect1.where("true").execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 3, testSelect1, 0, -1); + assertTestPreparedStatementsResult(testSelect2.where("true AND ord >= :n").bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect2, 0, -1); + assertTestPreparedStatementsResult(testSelect3.where("true AND ord >= :n AND ord <= :n + 3").bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect3, 0, -1); + assertTestPreparedStatementsResult(testSelect4.where("true AND ord >= :n AND ord <= :m").bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect1, 1, 1); + assertTestPreparedStatementsResult(testSelect2.bind("n", 4).execute(), 4, 8); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect2, 2, 1); + assertTestPreparedStatementsResult(testSelect3.bind("n", 4).execute(), 4, 7); + assertPreparedStatementsCountsAndId(testSession, 3, testSelect3, 3, 1); + assertTestPreparedStatementsResult(testSelect4.bind("n", 3).bind("m", 7).execute(), 3, 7); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testSelect1.limit(2).execute(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect1, 1, 1); + assertTestPreparedStatementsResult(testSelect2.limit(2).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect2, 2, 1); + assertTestPreparedStatementsResult(testSelect3.limit(2).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect3, 3, 1); + assertTestPreparedStatementsResult(testSelect4.limit(2).execute(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + + // H. Set limit and offset reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testSelect1.limit(1).offset(1).execute(), 2, 2); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect1, 1, 2); + assertTestPreparedStatementsResult(testSelect2.limit(1).offset(1).execute(), 5, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect2, 2, 2); + assertTestPreparedStatementsResult(testSelect3.limit(1).offset(1).execute(), 5, 5); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect3, 3, 2); + assertTestPreparedStatementsResult(testSelect4.limit(1).offset(1).execute(), 4, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + + // I. Set orderBy deallocates and resets execution count, set limit and bind has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testSelect1.orderBy("id").limit(2).execute(), 2, 3); + assertPreparedStatementsCountsAndId(testSession, 3, testSelect1, 0, -1); + assertTestPreparedStatementsResult(testSelect2.orderBy("id").limit(2).bind("n", 4).execute(), 5, 6); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect2, 0, -1); + assertTestPreparedStatementsResult(testSelect3.orderBy("id").limit(2).bind("n", 4).execute(), 5, 6); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect3, 0, -1); + assertTestPreparedStatementsResult(testSelect4.orderBy("id").limit(2).bind("m", 7).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + + // J. Set offset reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testSelect1.offset(0).execute(), 1, 2); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect1, 1, 1); + assertTestPreparedStatementsResult(testSelect2.offset(0).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 2, testSelect2, 2, 1); + assertTestPreparedStatementsResult(testSelect3.offset(0).execute(), 4, 5); + assertPreparedStatementsCountsAndId(testSession, 3, testSelect3, 3, 1); + assertTestPreparedStatementsResult(testSelect4.offset(0).execute(), 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testSelect4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testTbl = testSession.getDefaultSchema().getTable("testPrepareSelect"); + + testSelect1 = testTbl.select("ord"); + testSelect2 = testTbl.select("ord"); + + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect1, 0, -1); + assertTestPreparedStatementsResult(testSelect2.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 0, testSelect2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect1, 1, 1); + assertTestPreparedStatementsResult(testSelect2.execute(), 1, 8); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testSelect2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testSelect1.execute(), 1, 8); + assertPreparedStatementsCountsAndId(testSession, 1, testSelect1, 1, 2); + assertTestPreparedStatementsResult(testSelect2.execute(), 1, 8); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testSelect2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } finally { + sqlUpdate("DROP TABLE IF EXISTS testPrepareSelect"); + } + } + + private void assertTestPreparedStatementsResult(RowResult res, int expectedMin, int expectedMax) { + for (Row r : res.fetchAll()) { + assertEquals(expectedMin++, r.getInt("ord")); + } + assertEquals(expectedMax, expectedMin - 1); + } } diff --git a/src/test/java/testsuite/x/devapi/TableUpdateTest.java b/src/test/java/testsuite/x/devapi/TableUpdateTest.java index 12242f0ad..4d535f270 100644 --- a/src/test/java/testsuite/x/devapi/TableUpdateTest.java +++ b/src/test/java/testsuite/x/devapi/TableUpdateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 2.0, as published by the @@ -35,9 +35,14 @@ import org.junit.Test; +import com.mysql.cj.ServerVersion; +import com.mysql.cj.xdevapi.Result; import com.mysql.cj.xdevapi.Row; import com.mysql.cj.xdevapi.RowResult; +import com.mysql.cj.xdevapi.Session; +import com.mysql.cj.xdevapi.SessionFactory; import com.mysql.cj.xdevapi.Table; +import com.mysql.cj.xdevapi.UpdateStatement; /** * @todo @@ -81,5 +86,249 @@ public void testUpdates() { } } - // TODO: there could be more tests, but I expect this API and implementation to change to better accommodate some "normal" use cases + @Test + public void testPreparedStatements() { + if (!this.isSetForXTests || !mysqlVersionMeetsMinimum(ServerVersion.parseVersion("8.0.14"))) { + return; + } + + try { + // Prepare test data. + testPreparedStatementsResetData(); + + SessionFactory sf = new SessionFactory(); + + /* + * Test common usage. + */ + Session testSession = sf.getSession(this.testProperties); + + int sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + Table testTbl1 = testSession.getDefaultSchema().getTable("testPrepareUpdate_1"); + Table testTbl2 = testSession.getDefaultSchema().getTable("testPrepareUpdate_2"); + Table testTbl3 = testSession.getDefaultSchema().getTable("testPrepareUpdate_3"); + Table testTbl4 = testSession.getDefaultSchema().getTable("testPrepareUpdate_4"); + + // Initialize several UpdateStatement objects. + UpdateStatement testUpdate1 = testTbl1.update().where("true").set("ord", expr("ord * 10")); // Update all. + UpdateStatement testUpdate2 = testTbl2.update().where("ord >= :n").set("ord", expr("ord * 10")); // Criteria with one placeholder. + UpdateStatement testupdate3 = testTbl3.update().where("ord >= :n AND ord <= :n + 1").set("ord", expr("ord * 10")); // Criteria with same placeholder repeated. + UpdateStatement testUpdate4 = testTbl4.update().where("ord >= :n AND ord <= :m").set("ord", expr("ord * 10")); // Criteria with multiple placeholders. + + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate1, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate2, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testupdate3, 0, -1); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate4, 0, -1); + + // A. Set binds: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate1, 0, -1); + assertTestPreparedStatementsResult(testUpdate2.bind("n", 2).execute(), 3, testTbl2.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate2, 0, -1); + assertTestPreparedStatementsResult(testupdate3.bind("n", 2).execute(), 2, testTbl3.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testupdate3, 0, -1); + assertTestPreparedStatementsResult(testUpdate4.bind("n", 2).bind("m", 3).execute(), 2, testTbl4.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // B. Set orderBy resets execution count: 1st execute -> non-prepared. + assertTestPreparedStatementsResult(testUpdate1.orderBy("id").execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate1, 0, -1); + assertTestPreparedStatementsResult(testUpdate2.orderBy("id").execute(), 3, testTbl2.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate2, 0, -1); + assertTestPreparedStatementsResult(testupdate3.orderBy("id").execute(), 2, testTbl3.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testupdate3, 0, -1); + assertTestPreparedStatementsResult(testUpdate4.orderBy("id").execute(), 2, testTbl4.getName(), 1, 20, 30, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // C. Set binds reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate1, 1, 1); + assertTestPreparedStatementsResult(testUpdate2.bind("n", 3).execute(), 2, testTbl2.getName(), 1, 2, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 2, testUpdate2, 2, 1); + assertTestPreparedStatementsResult(testupdate3.bind("n", 3).execute(), 2, testTbl3.getName(), 1, 2, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 3, testupdate3, 3, 1); + assertTestPreparedStatementsResult(testUpdate4.bind("m", 4).execute(), 3, testTbl4.getName(), 1, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 4, 4, 0); + testPreparedStatementsResetData(); + + // D. Set binds reuse statement: 3rd execute -> execute. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate1, 1, 2); + assertTestPreparedStatementsResult(testUpdate2.bind("n", 4).execute(), 1, testTbl2.getName(), 1, 2, 3, 40); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate2, 2, 2); + assertTestPreparedStatementsResult(testupdate3.bind("n", 1).execute(), 2, testTbl3.getName(), 10, 20, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testupdate3, 3, 2); + assertTestPreparedStatementsResult(testUpdate4.bind("m", 2).execute(), 1, testTbl4.getName(), 1, 20, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 0); + testPreparedStatementsResetData(); + + // E. Set new values deallocates and resets execution count: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testUpdate1.set("ord", expr("ord * 100")).execute(), 4, testTbl1.getName(), 100, 200, 300, 400); + assertPreparedStatementsCountsAndId(testSession, 3, testUpdate1, 0, -1); + assertTestPreparedStatementsResult(testUpdate2.set("ord", expr("ord * 100")).execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testUpdate2, 0, -1); + assertTestPreparedStatementsResult(testupdate3.set("ord", expr("ord * 100")).execute(), 2, testTbl3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testupdate3, 0, -1); + assertTestPreparedStatementsResult(testUpdate4.set("ord", expr("ord * 100")).execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 4, 8, 4); + testPreparedStatementsResetData(); + + // F. No Changes: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 100, 200, 300, 400); + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate1, 1, 1); + assertTestPreparedStatementsResult(testUpdate2.execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testUpdate2, 2, 1); + assertTestPreparedStatementsResult(testupdate3.execute(), 2, testTbl3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testupdate3, 3, 1); + assertTestPreparedStatementsResult(testUpdate4.execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 8, 12, 4); + testPreparedStatementsResetData(); + + // G. Set limit for the first time deallocates and re-prepares: 1st execute -> re-prepare + execute. + assertTestPreparedStatementsResult(testUpdate1.limit(1).execute(), 1, testTbl1.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate1, 1, 1); + assertTestPreparedStatementsResult(testUpdate2.limit(1).execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate2, 2, 1); + assertTestPreparedStatementsResult(testupdate3.limit(1).execute(), 1, testTbl3.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testupdate3, 3, 1); + assertTestPreparedStatementsResult(testUpdate4.limit(1).execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 12, 16, 8); + testPreparedStatementsResetData(); + + // H. Set limit reuse prepared statement: 2nd execute -> execute. + assertTestPreparedStatementsResult(testUpdate1.limit(2).execute(), 2, testTbl1.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate1, 1, 2); + assertTestPreparedStatementsResult(testUpdate2.limit(2).execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate2, 2, 2); + assertTestPreparedStatementsResult(testupdate3.limit(2).execute(), 2, testTbl3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testupdate3, 3, 2); + assertTestPreparedStatementsResult(testUpdate4.limit(2).execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 2); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 8); + testPreparedStatementsResetData(); + + // I. Set orderBy deallocates and resets execution count, set limit has no effect: 1st execute -> deallocate + non-prepared. + assertTestPreparedStatementsResult(testUpdate1.orderBy("id").limit(1).execute(), 1, testTbl1.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testUpdate1, 0, -1); + assertTestPreparedStatementsResult(testUpdate2.orderBy("id").limit(1).execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testUpdate2, 0, -1); + assertTestPreparedStatementsResult(testupdate3.orderBy("id").limit(1).execute(), 1, testTbl3.getName(), 100, 2, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testupdate3, 0, -1); + assertTestPreparedStatementsResult(testUpdate4.orderBy("id").limit(1).execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate4, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 12, 20, 12); + testPreparedStatementsResetData(); + + // J. Set limit reuse statement: 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testUpdate1.limit(2).execute(), 2, testTbl1.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate1, 1, 1); + assertTestPreparedStatementsResult(testUpdate2.limit(2).execute(), 1, testTbl2.getName(), 1, 2, 3, 400); + assertPreparedStatementsCountsAndId(testSession, 2, testUpdate2, 2, 1); + assertTestPreparedStatementsResult(testupdate3.limit(2).execute(), 2, testTbl3.getName(), 100, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 3, testupdate3, 3, 1); + assertTestPreparedStatementsResult(testUpdate4.limit(2).execute(), 1, testTbl4.getName(), 1, 200, 3, 4); + assertPreparedStatementsCountsAndId(testSession, 4, testUpdate4, 4, 1); + + assertPreparedStatementsStatusCounts(testSession, 16, 24, 12); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + + /* + * Test falling back onto non-prepared statements. + */ + testSession = sf.getSession(this.testProperties); + int origMaxPrepStmtCount = this.session.sql("SELECT @@max_prepared_stmt_count").execute().fetchOne().getInt(0); + + try { + // Allow preparing only one more statement. + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(getPreparedStatementsCount() + 1).execute(); + + sessionThreadId = getThreadId(testSession); + assertPreparedStatementsCount(sessionThreadId, 0, 1); + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + + testTbl1 = testSession.getDefaultSchema().getTable("testPrepareUpdate_1"); + testTbl2 = testSession.getDefaultSchema().getTable("testPrepareUpdate_2"); + + testUpdate1 = testTbl1.update().where("true").set("ord", expr("ord * 10")); + testUpdate2 = testTbl2.update().where("true").set("ord", expr("ord * 10")); + + // 1st execute -> don't prepare. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate1, 0, -1); + assertTestPreparedStatementsResult(testUpdate2.execute(), 4, testTbl2.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 0, testUpdate2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 0, 0, 0); + testPreparedStatementsResetData(); + + // 2nd execute -> prepare + execute. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate1, 1, 1); + assertTestPreparedStatementsResult(testUpdate2.execute(), 4, testTbl2.getName(), 10, 20, 30, 40); // Fails preparing, execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 1, 0); // Failed prepare also counts. + testPreparedStatementsResetData(); + + // 3rd execute -> execute. + assertTestPreparedStatementsResult(testUpdate1.execute(), 4, testTbl1.getName(), 10, 20, 30, 40); + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate1, 1, 2); + assertTestPreparedStatementsResult(testUpdate2.execute(), 4, testTbl2.getName(), 10, 20, 30, 40); // Execute as non-prepared. + assertPreparedStatementsCountsAndId(testSession, 1, testUpdate2, 0, -1); + + assertPreparedStatementsStatusCounts(testSession, 2, 2, 0); + testPreparedStatementsResetData(); + + testSession.close(); + assertPreparedStatementsCount(sessionThreadId, 0, 10); // Prepared statements won't live past the closing of the session. + } finally { + this.session.sql("SET GLOBAL max_prepared_stmt_count = ?").bind(origMaxPrepStmtCount).execute(); + } + } finally { + for (int i = 0; i < 4; i++) { + sqlUpdate("DROP TABLE IF EXISTS testPrepareUpdate_" + (i + 1)); + } + } + } + + private void testPreparedStatementsResetData() { + for (int i = 0; i < 4; i++) { + sqlUpdate("CREATE TABLE IF NOT EXISTS testPrepareUpdate_" + (i + 1) + " (id INT PRIMARY KEY, ord INT)"); + sqlUpdate("TRUNCATE TABLE testPrepareUpdate_" + (i + 1)); + sqlUpdate("INSERT INTO testPrepareUpdate_" + (i + 1) + " VALUES (1, 1), (2, 2), (3, 3), (4, 4)"); + } + } + + private void assertTestPreparedStatementsResult(Result res, int expectedAffectedItemsCount, String tableName, int... expectedValues) { + assertEquals(expectedAffectedItemsCount, res.getAffectedItemsCount()); + RowResult rowRes = this.schema.getTable(tableName).select("ord").execute(); + assertEquals(expectedValues.length, rowRes.count()); + for (int v : expectedValues) { + assertEquals(v, rowRes.next().getInt("ord")); + } + } }