1 /*
2  *  Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License, version 2.0,
6  *  as published by the Free Software Foundation.
7  *
8  *  This program is also distributed with certain software (including
9  *  but not limited to OpenSSL) that is licensed under separate terms,
10  *  as designated in a particular file or component or in included license
11  *  documentation.  The authors of MySQL hereby grant you an additional
12  *  permission to link the program and your derivative works with the
13  *  separately licensed software that they have included with MySQL.
14  *
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU General Public License, version 2.0, for more details.
19  *
20  *  You should have received a copy of the GNU General Public License
21  *  along with this program; if not, write to the Free Software
22  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
23  */
24 
25 package com.mysql.clusterj.jdbc;
26 
27 import com.mysql.clusterj.ClusterJFatalInternalException;
28 import com.mysql.clusterj.ClusterJUserException;
29 import com.mysql.clusterj.core.query.QueryDomainTypeImpl;
30 import com.mysql.clusterj.core.query.QueryExecutionContextImpl;
31 import com.mysql.clusterj.core.spi.DomainTypeHandler;
32 import com.mysql.clusterj.core.spi.QueryExecutionContext;
33 import com.mysql.clusterj.core.spi.SessionSPI;
34 import com.mysql.clusterj.core.spi.ValueHandler;
35 import com.mysql.clusterj.core.store.ResultData;
36 import com.mysql.clusterj.core.util.I18NHelper;
37 import com.mysql.clusterj.core.util.Logger;
38 import com.mysql.clusterj.core.util.LoggerFactoryService;
39 import com.mysql.jdbc.ParameterBindings;
40 import com.mysql.jdbc.ResultSetInternalMethods;
41 
42 import java.sql.SQLException;
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 
48 /** This class contains behavior to execute various SQL commands. There is one subclass for each
49  * command to be executed.
50  */
51 public class SQLExecutor {
52 
53     /** My message translator */
54     static final I18NHelper local = I18NHelper.getInstance(SQLExecutor.class);
55 
56     /** My logger */
57     static final Logger logger = LoggerFactoryService.getFactory().getInstance(SQLExecutor.class);
58 
59     /** The domain type handler for this SQL statement */
60     DomainTypeHandler<?> domainTypeHandler = null;
61 
62     /** The column names in the SQL statement */
63     protected List<String> columnNames = null;
64 
65     /** The number of fields in the domain object (also the number of mapped columns) */
66     protected int numberOfFields;
67 
68     /** The number of parameters in the where clause */
69     protected int numberOfParameters;
70 
71     /** The map of field numbers to parameter numbers */
72     protected int[] fieldNumberToColumnNumberMap = null;
73 
74     /** The map of column numbers to field numbers */
75     protected int[] columnNumberToFieldNumberMap = null;
76 
77     /** The map of column names to parameter numbers */
78     protected Map<String, Integer> columnNameToFieldNumberMap = new HashMap<String, Integer>();
79 
80     /** The query domain type for qualified SELECT and DELETE operations */
81     protected QueryDomainTypeImpl<?> queryDomainType;
82 
SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames, int numberOfParameters)83     public SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames, int numberOfParameters) {
84         this(domainTypeHandler, columnNames);
85         this.numberOfParameters = numberOfParameters;
86     }
87 
SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames)88     public SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames) {
89         this.domainTypeHandler = domainTypeHandler;
90         this.columnNames  = columnNames;
91         initializeFieldNumberMap();
92     }
93 
SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler)94     public SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler) {
95         this.domainTypeHandler = domainTypeHandler;
96     }
97 
SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames, QueryDomainTypeImpl<?> queryDomainType)98     public SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames,
99             QueryDomainTypeImpl<?> queryDomainType) {
100         this(domainTypeHandler, columnNames);
101         this.queryDomainType = queryDomainType;
102         initializeFieldNumberMap();
103     }
104 
SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, QueryDomainTypeImpl<?> queryDomainType, int numberOfParameters)105     public SQLExecutor(DomainTypeHandlerImpl<?> domainTypeHandler, QueryDomainTypeImpl<?> queryDomainType,
106             int numberOfParameters) {
107         this.domainTypeHandler = domainTypeHandler;
108         this.queryDomainType = queryDomainType;
109         this.numberOfParameters = numberOfParameters;
110     }
111 
112     /** This is the public interface exposed to other parts of the component. Calling this
113      * method executes the SQL statement via the clusterj api, or returns null indicating that
114      * the JDBC driver should execute the SQL statement.
115      */
116     public interface Executor {
117 
118         /** Execute the SQL command
119          * @param session the clusterj session which must not be null
120          * @param parameterBindings the parameter bindings from the prepared statement
121          * @return the result of executing the statement, or null
122          * @throws SQLException
123          */
execute(InterceptorImpl interceptor, ParameterBindings parameterBindings)124         ResultSetInternalMethods execute(InterceptorImpl interceptor,
125                 ParameterBindings parameterBindings) throws SQLException;
126     }
127 
128     /** This class implements the Executor contract but returns null, indicating that
129      * the JDBC driver should implement the call itself.
130      */
131     public static class Noop implements Executor {
132 
execute(InterceptorImpl interceptor, ParameterBindings parameterBindings)133         public ResultSetInternalMethods execute(InterceptorImpl interceptor,
134                 ParameterBindings parameterBindings) throws SQLException {
135             return null;
136         }
137     }
138 
139     /** This class implements the Executor contract for Select operations.
140      */
141     public static class Select extends SQLExecutor implements Executor {
142 
Select(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames, QueryDomainTypeImpl<?> queryDomainType)143         public Select(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames, QueryDomainTypeImpl<?> queryDomainType) {
144             super(domainTypeHandler, columnNames, queryDomainType);
145             if (queryDomainType == null) {
146                 throw new ClusterJFatalInternalException("queryDomainType must not be null for Select.");
147             }
148         }
149 
execute(InterceptorImpl interceptor, ParameterBindings parameterBindings)150         public ResultSetInternalMethods execute(InterceptorImpl interceptor,
151                 ParameterBindings parameterBindings) throws SQLException {
152             // create value handler to copy data from parameters to ndb
153             // count the parameters
154             int count = countParameters(parameterBindings);
155             SessionSPI session = interceptor.getSession();
156             Map<String, Object> parameters = createParameterMap(queryDomainType, parameterBindings, 0, count);
157             QueryExecutionContext context = new QueryExecutionContextImpl(session, parameters);
158             session.startAutoTransaction();
159             try {
160                 ResultData resultData = queryDomainType.getResultData(context);
161                 // session.endAutoTransaction();
162                 return new ResultSetInternalMethodsImpl(resultData, columnNumberToFieldNumberMap,
163                         columnNameToFieldNumberMap, session);
164             } catch (Exception e) {
165                 e.printStackTrace();
166                 session.failAutoTransaction();
167                 return null;
168             }
169         }
170     }
171 
172     /** This class implements the Executor contract for Delete operations.
173      */
174     public static class Delete extends SQLExecutor implements Executor {
175 
Delete(DomainTypeHandlerImpl<?> domainTypeHandler, QueryDomainTypeImpl<?> queryDomainType, int numberOfParameters)176         public Delete (DomainTypeHandlerImpl<?> domainTypeHandler, QueryDomainTypeImpl<?> queryDomainType,
177                 int numberOfParameters) {
178             super(domainTypeHandler, queryDomainType, numberOfParameters);
179         }
180 
Delete(DomainTypeHandlerImpl<?> domainTypeHandler)181         public Delete (DomainTypeHandlerImpl<?> domainTypeHandler) {
182             super(domainTypeHandler);
183         }
184 
execute(InterceptorImpl interceptor, ParameterBindings parameterBindings)185         public ResultSetInternalMethods execute(InterceptorImpl interceptor,
186                 ParameterBindings parameterBindings) throws SQLException {
187             SessionSPI session = interceptor.getSession();
188             if (queryDomainType == null) {
189                 int rowsDeleted = session.deletePersistentAll(domainTypeHandler);
190                 if (logger.isDebugEnabled()) logger.debug("deleteAll deleted: " + rowsDeleted);
191                 return new ResultSetInternalMethodsUpdateCount(rowsDeleted);
192             } else {
193                 int numberOfBoundParameters = countParameters(parameterBindings);
194                 int numberOfStatements = numberOfBoundParameters / numberOfParameters;
195                 long[] deleteResults = new long[numberOfStatements];
196                 if (logger.isDebugEnabled()) logger.debug(
197                         " numberOfParameters: " + numberOfParameters
198                         + " numberOfBoundParameters: " + numberOfBoundParameters
199                         + " numberOfStatements: " + numberOfStatements
200                         );
201                 QueryExecutionContextJDBCImpl context =
202                     new QueryExecutionContextJDBCImpl(session, parameterBindings, numberOfParameters);
203                 for (int i = 0; i < numberOfStatements; ++i) {
204                     // this will execute each statement in the batch using different parameters
205                     int statementRowsDeleted = queryDomainType.deletePersistentAll(context);
206                     if (logger.isDebugEnabled()) logger.debug("statement " + i
207                             + " deleted " + statementRowsDeleted);
208                     deleteResults[i] = statementRowsDeleted;
209                     context.nextStatement();
210                 }
211                 return new ResultSetInternalMethodsUpdateCount(deleteResults);
212             }
213         }
214     }
215 
216     /** This class implements the Executor contract for Insert operations.
217      */
218     public static class Insert extends SQLExecutor implements Executor {
219 
Insert(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames)220         public Insert(DomainTypeHandlerImpl<?> domainTypeHandler, List<String> columnNames) {
221             super(domainTypeHandler, columnNames, columnNames.size());
222         }
223 
execute(InterceptorImpl interceptor, ParameterBindings parameterBindings)224         public ResultSetInternalMethods execute(InterceptorImpl interceptor,
225                 ParameterBindings parameterBindings) throws SQLException {
226             SessionSPI session = interceptor.getSession();
227             int numberOfBoundParameters = countParameters(parameterBindings);
228             int numberOfStatements = numberOfBoundParameters / numberOfParameters;
229             if (logger.isDebugEnabled()) logger.debug("SQLExecutor.Insert.execute"
230                     + " numberOfParameters: " + numberOfParameters
231                     + " numberOfBoundParameters: " + numberOfBoundParameters
232                     + " numberOfFields: " + numberOfFields
233                     + " numberOfStatements: " + numberOfStatements
234                     );
235             // interceptor.beforeClusterjStart();
236             // session asks for values by field number which are converted to parameter number
237             for (int offset = 0; offset < numberOfBoundParameters; offset += numberOfParameters) {
238                 ValueHandler valueHandler = getValueHandler(parameterBindings, fieldNumberToColumnNumberMap, offset);
239                 session.insert(domainTypeHandler, valueHandler);
240             }
241             session.flush();
242             // interceptor.afterClusterjStart();
243             return new ResultSetInternalMethodsUpdateCount(numberOfStatements);
244         }
245     }
246 
247     /** Create the parameter map assigning each bound parameter a number.
248      * The result is a map in which the key is a String whose key is a cardinal number
249      * starting with 1 (for JDBC which uses 1-origin for numbering)
250      * and whose value is the parameter's value.
251      * @param queryDomainType the query domain type
252      * @param parameterBindings the parameter bindings
253      * @param offset the number of parameters to skip
254      * @param count the number of parameters to use
255      * @return
256      * @throws SQLException
257      */
createParameterMap(QueryDomainTypeImpl<?> queryDomainType, ParameterBindings parameterBindings, int offset, int count)258     protected Map<String, Object> createParameterMap(QueryDomainTypeImpl<?> queryDomainType,
259             ParameterBindings parameterBindings, int offset, int count) throws SQLException {
260         Map<String, Object> result = new HashMap<String, Object>();
261         int first = offset + 1;
262         int last = offset + count + 1;
263         for (int i = first; i < last; ++i) {
264             Object placeholder = parameterBindings.getObject(i);
265             if (logger.isDetailEnabled())
266                 logger.detail("Put placeholder " + i + " value: " + placeholder + " of type " + placeholder.getClass());
267             result.put(String.valueOf(i), placeholder);
268         }
269         return result;
270     }
271 
272     /** Initialize the mappings between the Java representation of the row (domain type handler)
273      * and the JDBC/database representation of the row. The JDBC driver asks for columns by column
274      * index or column name, 1-origin. The domain type handler returns data by field number, 0-origin.
275      * The domain type handler has representations for all columns in the database, whereas the JDBC
276      * driver has a specific set of columns referenced by the SQL statement.
277      * For insert, the column number to field number mapping will map parameters to field numbers,
278      * e.g. INSERT INTO EMPLOYEE (id, name, age) VALUES (?, ?, ?)
279      * For select, the column number to field number mapping will map result set columns to field numbers,
280      * e.g. SELECT id, name, age FROM EMPLOYEE
281      */
initializeFieldNumberMap()282     private void initializeFieldNumberMap() {
283         // the index into the int[] is the 0-origin field number (columns in order of definition in the schema)
284         // the value is the index into the parameter bindings (columns in order of the sql insert statement)
285         String[] fieldNames = domainTypeHandler.getFieldNames();
286         numberOfFields = fieldNames.length;
287         fieldNumberToColumnNumberMap = new int[numberOfFields];
288         columnNumberToFieldNumberMap = new int[1 + columnNames.size()];
289         for (int i= 0; i < numberOfFields; ++i) {
290             columnNameToFieldNumberMap.put(fieldNames[i], i);
291             int index = columnNames.indexOf(fieldNames[i]);
292             if (index >= 0) {
293                 // index origin 1 for JDBC interfaces
294                 fieldNumberToColumnNumberMap[i] = index + 1;
295                 columnNumberToFieldNumberMap[index + 1] = i;
296             } else {
297                 // field is not in column list
298                 fieldNumberToColumnNumberMap[i] = -1;
299             }
300         }
301         // make sure all columns are fields and if not, throw an exception
302         for (String columnName: columnNames) {
303             if (columnNameToFieldNumberMap.get(columnName) == null) {
304                 throw new ClusterJUserException(
305                         local.message("ERR_Column_Name_Not_In_Table", columnName,
306                         Arrays.toString(fieldNames),
307                         domainTypeHandler.getTableName()));
308             }
309         }
310         if (logger.isDetailEnabled()) {
311             StringBuilder buffer = new StringBuilder();
312             for (int i = 0; i < fieldNumberToColumnNumberMap.length; ++i) {
313                 int columnNumber = fieldNumberToColumnNumberMap[i];
314                 buffer.append("field ");
315                 buffer.append(i);
316                 buffer.append(" mapped to ");
317                 buffer.append(columnNumber);
318                 buffer.append("[");
319                 buffer.append(columnNumber == -1?"nothing":(columnNames.get(columnNumber - 1)));
320                 buffer.append("];");
321             }
322             logger.detail(buffer.toString());
323         }
324     }
325 
326     /** Create a value handler (part of the clusterj spi) to retrieve values from jdbc parameter bindings.
327      * @param parameterBindings the jdbc parameter bindings from prepared statements
328      * @param fieldNumberToParameterNumberMap map from field number to parameter number
329      * @param offset into the parameter bindings for this instance (used for batch execution)
330      * @return
331      */
getValueHandler(ParameterBindings parameterBindings, int[] fieldNumberToParameterNumberMap, int offset)332     protected ValueHandler getValueHandler(ParameterBindings parameterBindings,
333             int[] fieldNumberToParameterNumberMap, int offset) {
334         return new ValueHandlerImpl(parameterBindings, fieldNumberToParameterNumberMap, offset);
335     }
336 
337     /** If detailed logging is enabled write the parameter bindings to the log.
338      * @param parameterBindings the jdbc parameter bindings
339      */
logParameterBindings(ParameterBindings parameterBindings)340     protected static void logParameterBindings(ParameterBindings parameterBindings) {
341         if (logger.isDetailEnabled()) {
342             int i = 0;
343             while (true) {
344                 try {
345                     String value = parameterBindings.getObject(++i).toString();
346                     // parameters are 1-origin per jdbc specification
347                     logger.detail("parameterBinding: parameter " + i + " has value: " + value);
348                 } catch (Exception e) {
349                     // we don't know how many parameters are bound...
350                     break;
351                 }
352             }
353         }
354     }
355 
356     /** Count the number of bound parameters. If this is a batch execution, then the
357      * number of bound parameters is the number of statements in the batch times the
358      * number of parameters per statement.
359      * If detailed logging is enabled write the parameter bindings to the log.
360      * @param parameterBindings the jdbc parameter bindings
361      */
countParameters(ParameterBindings parameterBindings)362     protected static int countParameters(ParameterBindings parameterBindings) {
363         int i = 0;
364         while (true) {
365             try {
366                 ++i;
367                 // parameters are 1-origin per jdbc specification
368                 Object objectValue = parameterBindings.getObject(i);
369                 if (logger.isDetailEnabled()) {
370                     logger.detail("parameterBinding: parameter " + i
371                             + " has value: " + objectValue
372                             + " of type " + objectValue.getClass());
373                 }
374             } catch (Exception e) {
375                 // we don't know how many parameters are bound...
376                 break;
377             }
378         }
379         return i - 1;
380     }
381 
382 }
383