001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.core.metadata;
018
019import java.sql.DatabaseMetaData;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Set;
028import javax.sql.DataSource;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032
033import org.springframework.dao.InvalidDataAccessApiUsageException;
034import org.springframework.jdbc.core.RowMapper;
035import org.springframework.jdbc.core.SqlOutParameter;
036import org.springframework.jdbc.core.SqlParameter;
037import org.springframework.jdbc.core.SqlParameterValue;
038import org.springframework.jdbc.core.SqlReturnResultSet;
039import org.springframework.jdbc.core.namedparam.SqlParameterSource;
040import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
041import org.springframework.jdbc.support.JdbcUtils;
042import org.springframework.util.StringUtils;
043
044/**
045 * Class to manage context meta-data used for the configuration
046 * and execution of a stored procedure call.
047 *
048 * @author Thomas Risberg
049 * @author Juergen Hoeller
050 * @author Kiril Nugmanov
051 * @since 2.5
052 */
053public class CallMetaDataContext {
054
055        // Logger available to subclasses
056        protected final Log logger = LogFactory.getLog(getClass());
057
058        // Name of procedure to call
059        private String procedureName;
060
061        // Name of catalog for call
062        private String catalogName;
063
064        // Name of schema for call
065        private String schemaName;
066
067        // List of SqlParameter objects to be used in call execution
068        private List<SqlParameter> callParameters = new ArrayList<SqlParameter>();
069
070        // Actual name to use for the return value in the output map
071        private String actualFunctionReturnName;
072
073        // Set of in parameter names to exclude use for any not listed
074        private Set<String> limitedInParameterNames = new HashSet<String>();
075
076        // List of SqlParameter names for out parameters
077        private List<String> outParameterNames = new ArrayList<String>();
078
079        // Indicates whether this is a procedure or a function
080        private boolean function = false;
081
082        // Indicates whether this procedure's return value should be included
083        private boolean returnValueRequired = false;
084
085        // Should we access call parameter meta-data info or not
086        private boolean accessCallParameterMetaData = true;
087
088        // Should we bind parameter by name
089        private boolean namedBinding;
090
091        // The provider of call meta-data
092        private CallMetaDataProvider metaDataProvider;
093
094
095        /**
096         * Specify the name used for the return value of the function.
097         */
098        public void setFunctionReturnName(String functionReturnName) {
099                this.actualFunctionReturnName = functionReturnName;
100        }
101
102        /**
103         * Get the name used for the return value of the function.
104         */
105        public String getFunctionReturnName() {
106                return (this.actualFunctionReturnName != null ? this.actualFunctionReturnName : "return");
107        }
108
109        /**
110         * Specify a limited set of in parameters to be used.
111         */
112        public void setLimitedInParameterNames(Set<String> limitedInParameterNames) {
113                this.limitedInParameterNames = limitedInParameterNames;
114        }
115
116        /**
117         * Get a limited set of in parameters to be used.
118         */
119        public Set<String> getLimitedInParameterNames() {
120                return this.limitedInParameterNames;
121        }
122
123        /**
124         * Specify the names of the out parameters.
125         */
126        public void setOutParameterNames(List<String> outParameterNames) {
127                this.outParameterNames = outParameterNames;
128        }
129
130        /**
131         * Get a list of the out parameter names.
132         */
133        public List<String> getOutParameterNames() {
134                return this.outParameterNames;
135        }
136
137        /**
138         * Specify the name of the procedure.
139         */
140        public void setProcedureName(String procedureName) {
141                this.procedureName = procedureName;
142        }
143
144        /**
145         * Get the name of the procedure.
146         */
147        public String getProcedureName() {
148                return this.procedureName;
149        }
150
151        /**
152         * Specify the name of the catalog.
153         */
154        public void setCatalogName(String catalogName) {
155                this.catalogName = catalogName;
156        }
157
158        /**
159         * Get the name of the catalog.
160         */
161        public String getCatalogName() {
162                return this.catalogName;
163        }
164
165        /**
166         * Secify the name of the schema.
167         */
168        public void setSchemaName(String schemaName) {
169                this.schemaName = schemaName;
170        }
171
172        /**
173         * Get the name of the schema.
174         */
175        public String getSchemaName() {
176                return this.schemaName;
177        }
178
179        /**
180         * Specify whether this call is a function call.
181         */
182        public void setFunction(boolean function) {
183                this.function = function;
184        }
185
186        /**
187         * Check whether this call is a function call.
188         */
189        public boolean isFunction() {
190                return this.function;
191        }
192
193        /**
194         * Specify whether a return value is required.
195         */
196        public void setReturnValueRequired(boolean returnValueRequired) {
197                this.returnValueRequired = returnValueRequired;
198        }
199
200        /**
201         * Check whether a return value is required.
202         */
203        public boolean isReturnValueRequired() {
204                return this.returnValueRequired;
205        }
206
207        /**
208         * Specify whether call parameter meta-data should be accessed.
209         */
210        public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) {
211                this.accessCallParameterMetaData = accessCallParameterMetaData;
212        }
213
214        /**
215         * Check whether call parameter meta-data should be accessed.
216         */
217        public boolean isAccessCallParameterMetaData() {
218                return this.accessCallParameterMetaData;
219        }
220
221        /**
222         * Specify whether parameters should be bound by name.
223         * @since 4.2
224         */
225        public void setNamedBinding(boolean namedBinding) {
226                this.namedBinding = namedBinding;
227        }
228
229        /**
230         * Check whether parameters should be bound by name.
231         * @since 4.2
232         */
233        public boolean isNamedBinding() {
234                return this.namedBinding;
235        }
236
237
238        /**
239         * Initialize this class with meta-data from the database.
240         * @param dataSource the DataSource used to retrieve meta-data
241         */
242        public void initializeMetaData(DataSource dataSource) {
243                this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this);
244        }
245
246        /**
247         * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided
248         * by the JDBC driver used for the database in use.
249         * @param parameterName the name of the parameter (also used as the name of the List returned in the output)
250         * @param rowMapper a RowMapper implementation used to map the data returned in the result set
251         * @return the appropriate SqlParameter
252         */
253        public SqlParameter createReturnResultSetParameter(String parameterName, RowMapper<?> rowMapper) {
254                if (this.metaDataProvider.isReturnResultSetSupported()) {
255                        return new SqlReturnResultSet(parameterName, rowMapper);
256                }
257                else {
258                        if (this.metaDataProvider.isRefCursorSupported()) {
259                                return new SqlOutParameter(parameterName, this.metaDataProvider.getRefCursorSqlType(), rowMapper);
260                        }
261                        else {
262                                throw new InvalidDataAccessApiUsageException(
263                                                "Return of a ResultSet from a stored procedure is not supported");
264                        }
265                }
266        }
267
268        /**
269         * Get the name of the single out parameter for this call.
270         * If there are multiple parameters, the name of the first one will be returned.
271         */
272        public String getScalarOutParameterName() {
273                if (isFunction()) {
274                        return getFunctionReturnName();
275                }
276                else {
277                        if (this.outParameterNames.size() > 1) {
278                                logger.warn("Accessing single output value when procedure has more than one output parameter");
279                        }
280                        return (!this.outParameterNames.isEmpty() ? this.outParameterNames.get(0) : null);
281                }
282        }
283
284        /**
285         * Get the List of SqlParameter objects to be used in call execution.
286         */
287        public List<SqlParameter> getCallParameters() {
288                return this.callParameters;
289        }
290
291        /**
292         * Process the list of parameters provided, and if procedure column meta-data is used,
293         * the parameters will be matched against the meta-data information and any missing
294         * ones will be automatically included.
295         * @param parameters the list of parameters to use as a base
296         */
297        public void processParameters(List<SqlParameter> parameters) {
298                this.callParameters = reconcileParameters(parameters);
299        }
300
301        /**
302         * Reconcile the provided parameters with available meta-data and add new ones where appropriate.
303         */
304        protected List<SqlParameter> reconcileParameters(List<SqlParameter> parameters) {
305                final List<SqlParameter> declaredReturnParams = new ArrayList<SqlParameter>();
306                final Map<String, SqlParameter> declaredParams = new LinkedHashMap<String, SqlParameter>();
307                boolean returnDeclared = false;
308                List<String> outParamNames = new ArrayList<String>();
309                List<String> metaDataParamNames = new ArrayList<String>();
310
311                // Get the names of the meta-data parameters
312                for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) {
313                        if (!meta.isReturnParameter()) {
314                                metaDataParamNames.add(meta.getParameterName().toLowerCase());
315                        }
316                }
317
318                // Separate implicit return parameters from explicit parameters...
319                for (SqlParameter param : parameters) {
320                        if (param.isResultsParameter()) {
321                                declaredReturnParams.add(param);
322                        }
323                        else {
324                                String paramName = param.getName();
325                                if (paramName == null) {
326                                        throw new IllegalArgumentException("Anonymous parameters not supported for calls - " +
327                                                        "please specify a name for the parameter of SQL type " + param.getSqlType());
328                                }
329                                String paramNameToMatch = this.metaDataProvider.parameterNameToUse(paramName).toLowerCase();
330                                declaredParams.put(paramNameToMatch, param);
331                                if (param instanceof SqlOutParameter) {
332                                        outParamNames.add(paramName);
333                                        if (isFunction() && !metaDataParamNames.contains(paramNameToMatch) && !returnDeclared) {
334                                                if (logger.isDebugEnabled()) {
335                                                        logger.debug("Using declared out parameter '" + paramName + "' for function return value");
336                                                }
337                                                setFunctionReturnName(paramName);
338                                                returnDeclared = true;
339                                        }
340                                }
341                        }
342                }
343                setOutParameterNames(outParamNames);
344
345                List<SqlParameter> workParams = new ArrayList<SqlParameter>();
346                workParams.addAll(declaredReturnParams);
347                if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) {
348                        workParams.addAll(declaredParams.values());
349                        return workParams;
350                }
351
352                Map<String, String> limitedInParamNamesMap = new HashMap<String, String>(this.limitedInParameterNames.size());
353                for (String limitedParamName : this.limitedInParameterNames) {
354                        limitedInParamNamesMap.put(
355                                        this.metaDataProvider.parameterNameToUse(limitedParamName).toLowerCase(), limitedParamName);
356                }
357
358                for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) {
359                        String paramNameToCheck = null;
360                        if (meta.getParameterName() != null) {
361                                paramNameToCheck = this.metaDataProvider.parameterNameToUse(meta.getParameterName()).toLowerCase();
362                        }
363                        String paramNameToUse = this.metaDataProvider.parameterNameToUse(meta.getParameterName());
364                        if (declaredParams.containsKey(paramNameToCheck) || (meta.isReturnParameter() && returnDeclared)) {
365                                SqlParameter param;
366                                if (meta.isReturnParameter()) {
367                                        param = declaredParams.get(getFunctionReturnName());
368                                        if (param == null && !getOutParameterNames().isEmpty()) {
369                                                param = declaredParams.get(getOutParameterNames().get(0).toLowerCase());
370                                        }
371                                        if (param == null) {
372                                                throw new InvalidDataAccessApiUsageException(
373                                                                "Unable to locate declared parameter for function return value - " +
374                                                                " add a SqlOutParameter with name '" + getFunctionReturnName() + "'");
375                                        }
376                                        else {
377                                                setFunctionReturnName(param.getName());
378                                        }
379                                }
380                                else {
381                                        param = declaredParams.get(paramNameToCheck);
382                                }
383                                if (param != null) {
384                                        workParams.add(param);
385                                        if (logger.isDebugEnabled()) {
386                                                logger.debug("Using declared parameter for '" +
387                                                                (paramNameToUse != null ? paramNameToUse : getFunctionReturnName()) + "'");
388                                        }
389                                }
390                        }
391                        else {
392                                if (meta.isReturnParameter()) {
393                                        // DatabaseMetaData.procedureColumnReturn or possibly procedureColumnResult
394                                        if (!isFunction() && !isReturnValueRequired() &&
395                                                        this.metaDataProvider.byPassReturnParameter(meta.getParameterName())) {
396                                                if (logger.isDebugEnabled()) {
397                                                        logger.debug("Bypassing meta-data return parameter for '" + meta.getParameterName() + "'");
398                                                }
399                                        }
400                                        else {
401                                                String returnNameToUse =
402                                                                (StringUtils.hasLength(meta.getParameterName()) ? paramNameToUse : getFunctionReturnName());
403                                                workParams.add(this.metaDataProvider.createDefaultOutParameter(returnNameToUse, meta));
404                                                if (isFunction()) {
405                                                        setFunctionReturnName(returnNameToUse);
406                                                        outParamNames.add(returnNameToUse);
407                                                }
408                                                if (logger.isDebugEnabled()) {
409                                                        logger.debug("Added meta-data return parameter for '" + returnNameToUse + "'");
410                                                }
411                                        }
412                                }
413                                else {
414                                        if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) {
415                                                workParams.add(this.metaDataProvider.createDefaultOutParameter(paramNameToUse, meta));
416                                                outParamNames.add(paramNameToUse);
417                                                if (logger.isDebugEnabled()) {
418                                                        logger.debug("Added meta-data out parameter for '" + paramNameToUse + "'");
419                                                }
420                                        }
421                                        else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) {
422                                                workParams.add(this.metaDataProvider.createDefaultInOutParameter(paramNameToUse, meta));
423                                                outParamNames.add(paramNameToUse);
424                                                if (logger.isDebugEnabled()) {
425                                                        logger.debug("Added meta-data in-out parameter for '" + paramNameToUse + "'");
426                                                }
427                                        }
428                                        else {
429                                                if (this.limitedInParameterNames.isEmpty() ||
430                                                                limitedInParamNamesMap.containsKey(paramNameToUse.toLowerCase())) {
431                                                        workParams.add(this.metaDataProvider.createDefaultInParameter(paramNameToUse, meta));
432                                                        if (logger.isDebugEnabled()) {
433                                                                logger.debug("Added meta-data in parameter for '" + paramNameToUse + "'");
434                                                        }
435                                                }
436                                                else {
437                                                        if (logger.isDebugEnabled()) {
438                                                                logger.debug("Limited set of parameters " + limitedInParamNamesMap.keySet() +
439                                                                                " skipped parameter for '" + paramNameToUse + "'");
440                                                        }
441                                                }
442                                        }
443                                }
444                        }
445                }
446
447                return workParams;
448        }
449
450        /**
451         * Match input parameter values with the parameters declared to be used in the call.
452         * @param parameterSource the input values
453         * @return a Map containing the matched parameter names with the value taken from the input
454         */
455        public Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) {
456                // For parameter source lookups we need to provide case-insensitive lookup support
457                // since the database meta-data is not necessarily providing case sensitive parameter names.
458                Map<String, String> caseInsensitiveParameterNames =
459                                SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource);
460
461                Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size());
462                Map<String, Object> matchedParameters = new HashMap<String, Object>(this.callParameters.size());
463                for (SqlParameter parameter : this.callParameters) {
464                        if (parameter.isInputValueProvided()) {
465                                String parameterName = parameter.getName();
466                                String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
467                                if (parameterNameToMatch != null) {
468                                        callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName);
469                                }
470                                if (parameterName != null) {
471                                        if (parameterSource.hasValue(parameterName)) {
472                                                matchedParameters.put(parameterName,
473                                                                SqlParameterSourceUtils.getTypedValue(parameterSource, parameterName));
474                                        }
475                                        else {
476                                                String lowerCaseName = parameterName.toLowerCase();
477                                                if (parameterSource.hasValue(lowerCaseName)) {
478                                                        matchedParameters.put(parameterName,
479                                                                        SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName));
480                                                }
481                                                else {
482                                                        String englishLowerCaseName = parameterName.toLowerCase(Locale.ENGLISH);
483                                                        if (parameterSource.hasValue(englishLowerCaseName)) {
484                                                                matchedParameters.put(parameterName,
485                                                                                SqlParameterSourceUtils.getTypedValue(parameterSource, englishLowerCaseName));
486                                                        }
487                                                        else {
488                                                                String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(parameterName);
489                                                                if (parameterSource.hasValue(propertyName)) {
490                                                                        matchedParameters.put(parameterName,
491                                                                                        SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName));
492                                                                }
493                                                                else {
494                                                                        if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) {
495                                                                                String sourceName = caseInsensitiveParameterNames.get(lowerCaseName);
496                                                                                matchedParameters.put(parameterName,
497                                                                                                SqlParameterSourceUtils.getTypedValue(parameterSource, sourceName));
498                                                                        }
499                                                                        else if (logger.isWarnEnabled()) {
500                                                                                logger.warn("Unable to locate the corresponding parameter value for '" +
501                                                                                                parameterName + "' within the parameter values provided: " +
502                                                                                                caseInsensitiveParameterNames.values());
503                                                                        }
504                                                                }
505                                                        }
506                                                }
507                                        }
508                                }
509                        }
510                }
511
512                if (logger.isDebugEnabled()) {
513                        logger.debug("Matching " + caseInsensitiveParameterNames.values() + " with " + callParameterNames.values());
514                        logger.debug("Found match for " + matchedParameters.keySet());
515                }
516                return matchedParameters;
517        }
518
519        /**
520         * Match input parameter values with the parameters declared to be used in the call.
521         * @param inParameters the input values
522         * @return a Map containing the matched parameter names with the value taken from the input
523         */
524        public Map<String, ?> matchInParameterValuesWithCallParameters(Map<String, ?> inParameters) {
525                if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) {
526                        return inParameters;
527                }
528
529                Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size());
530                for (SqlParameter parameter : this.callParameters) {
531                        if (parameter.isInputValueProvided()) {
532                                String parameterName =  parameter.getName();
533                                String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
534                                if (parameterNameToMatch != null) {
535                                        callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName);
536                                }
537                        }
538                }
539
540                Map<String, Object> matchedParameters = new HashMap<String, Object>(inParameters.size());
541                for (String parameterName : inParameters.keySet()) {
542                        String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
543                        String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase());
544                        if (callParameterName == null) {
545                                if (logger.isDebugEnabled()) {
546                                        Object value = inParameters.get(parameterName);
547                                        if (value instanceof SqlParameterValue) {
548                                                value = ((SqlParameterValue) value).getValue();
549                                        }
550                                        if (value != null) {
551                                                logger.debug("Unable to locate the corresponding IN or IN-OUT parameter for \"" +
552                                                                parameterName + "\" in the parameters used: " + callParameterNames.keySet());
553                                        }
554                                }
555                        }
556                        else {
557                                matchedParameters.put(callParameterName, inParameters.get(parameterName));
558                        }
559                }
560
561                if (matchedParameters.size() < callParameterNames.size()) {
562                        for (String parameterName : callParameterNames.keySet()) {
563                                String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
564                                String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase());
565                                if (!matchedParameters.containsKey(callParameterName) && logger.isWarnEnabled()) {
566                                        logger.warn("Unable to locate the corresponding parameter value for '" + parameterName +
567                                                        "' within the parameter values provided: " + inParameters.keySet());
568                                }
569                        }
570                }
571
572                if (logger.isDebugEnabled()) {
573                        logger.debug("Matching " + inParameters.keySet() + " with " + callParameterNames.values());
574                        logger.debug("Found match for " + matchedParameters.keySet());
575                }
576                return matchedParameters;
577        }
578
579        public Map<String, ?> matchInParameterValuesWithCallParameters(Object[] parameterValues) {
580                Map<String, Object> matchedParameters = new HashMap<String, Object>(parameterValues.length);
581                int i = 0;
582                for (SqlParameter parameter : this.callParameters) {
583                        if (parameter.isInputValueProvided()) {
584                                String parameterName =  parameter.getName();
585                                matchedParameters.put(parameterName, parameterValues[i++]);
586                        }
587                }
588                return matchedParameters;
589        }
590
591        /**
592         * Build the call string based on configuration and meta-data information.
593         * @return the call string to be used
594         */
595        public String createCallString() {
596                StringBuilder callString;
597                int parameterCount = 0;
598                String catalogNameToUse;
599                String schemaNameToUse;
600
601                // For Oracle where catalogs are not supported we need to reverse the schema name
602                // and the catalog name since the cataog is used for the package name
603                if (this.metaDataProvider.isSupportsSchemasInProcedureCalls() &&
604                                !this.metaDataProvider.isSupportsCatalogsInProcedureCalls()) {
605                        schemaNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName());
606                        catalogNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
607                }
608                else {
609                        catalogNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName());
610                        schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
611                }
612
613                String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
614                if (isFunction() || isReturnValueRequired()) {
615                        callString = new StringBuilder().append("{? = call ").
616                                        append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
617                                        append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
618                                        append(procedureNameToUse).append("(");
619                        parameterCount = -1;
620                }
621                else {
622                        callString = new StringBuilder().append("{call ").
623                                        append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
624                                        append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
625                                        append(procedureNameToUse).append("(");
626                }
627
628                for (SqlParameter parameter : this.callParameters) {
629                        if (!parameter.isResultsParameter()) {
630                                if (parameterCount > 0) {
631                                        callString.append(", ");
632                                }
633                                if (parameterCount >= 0) {
634                                        callString.append(createParameterBinding(parameter));
635                                }
636                                parameterCount++;
637                        }
638                }
639                callString.append(")}");
640
641                return callString.toString();
642        }
643
644        /**
645         * Build the parameter binding fragment.
646         * @param parameter call parameter
647         * @return parameter binding fragment
648         * @since 4.2
649         */
650        protected String createParameterBinding(SqlParameter parameter) {
651                return (isNamedBinding() ? parameter.getName() + " => ?" : "?");
652        }
653
654}