View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2005, DbUnit.org
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2.1 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   *
20   */
21  
22  package org.dbunit.util;
23  
24  import java.io.PrintStream;
25  import java.sql.Connection;
26  import java.sql.DatabaseMetaData;
27  import java.sql.ResultSet;
28  import java.sql.SQLException;
29  import java.sql.Statement;
30  import java.util.Locale;
31  
32  import org.dbunit.DatabaseUnitRuntimeException;
33  import org.dbunit.database.IMetadataHandler;
34  import org.dbunit.dataset.Column;
35  import org.dbunit.dataset.ColumnMetaData;
36  import org.dbunit.dataset.datatype.DataType;
37  import org.dbunit.dataset.datatype.DataTypeException;
38  import org.dbunit.dataset.datatype.IDataTypeFactory;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Helper for SQL-related stuff.
44   * <br>
45   * TODO: testcases, also think about refactoring so that methods are not static anymore (for better extensibility)
46   * @author Felipe Leme (dbunit@felipeal.net)
47   * @version $Revision$
48   * @since Nov 5, 2005
49   * 
50   */
51  public class SQLHelper {
52  
53      /**
54       * The database product name reported by Sybase JDBC drivers.
55       */
56      public static final String DB_PRODUCT_SYBASE = "Sybase";
57  
58      /**
59       * Logger for this class
60       */
61      private static final Logger logger = LoggerFactory.getLogger(SQLHelper.class);
62  
63      // class is "static"
64      private SQLHelper() {}
65  
66      /**
67       * Gets the primary column for a table.
68       * @param conn connection with the database
69       * @param table table name
70       * @return name of primary column for a table (assuming it's just 1 column).
71       * @throws SQLException raised while getting the meta data
72       */
73      public static String getPrimaryKeyColumn( Connection conn, String table ) throws SQLException {
74          logger.debug("getPrimaryKeyColumn(conn={}, table={}) - start", conn, table);
75  
76          DatabaseMetaData metadata = conn.getMetaData();
77          ResultSet rs = metadata.getPrimaryKeys( null, null, table );
78          rs.next();
79          String pkColumn = rs.getString(4);
80          return pkColumn;
81      }
82  
83      /**
84       * Close a result set and a prepared statement, checking for null references.
85       * @param rs result set to be closed
86       * @param stmt prepared statement to be closed
87       * @throws SQLException exception raised in either close() method
88       */
89      public static void close(ResultSet rs, Statement stmt) throws SQLException {
90          logger.debug("close(rs={}, stmt={}) - start", rs, stmt);
91  
92          try {
93              SQLHelper.close(rs);
94          } finally {
95              SQLHelper.close( stmt );
96          }
97      }
98  
99      /**
100      * Close a SQL statement, checking for null references.
101      * @param stmt statement to be closed
102      * @throws SQLException exception raised while closing the statement
103      */
104     public static void close(Statement stmt) throws SQLException {
105         logger.debug("close(stmt={}) - start", stmt);
106 
107         if ( stmt != null ) {
108             stmt.close();
109         }
110     }
111 
112     /**
113      * Closes the given result set in a null-safe way
114      * @param resultSet
115      * @throws SQLException
116      */
117     public static void close(ResultSet resultSet) throws SQLException {
118         logger.debug("close(resultSet={}) - start", resultSet);
119 
120         if(resultSet != null) {
121             resultSet.close();
122         }
123     }
124 
125     /**
126      * Returns <code>true</code> if the given schema exists for the given connection.
127      * @param connection The connection to a database
128      * @param schema The schema to be searched
129      * @return Returns <code>true</code> if the given schema exists for the given connection.
130      * @throws SQLException
131      * @since 2.3.0
132      */
133     public static boolean schemaExists(Connection connection, String schema)
134             throws SQLException
135             {
136         logger.trace("schemaExists(connection={}, schema={}) - start", connection, schema);
137 
138         if(schema == null)
139         {
140             throw new NullPointerException("The parameter 'schema' must not be null");
141         }
142 
143         DatabaseMetaData metaData = connection.getMetaData();
144         ResultSet rs = metaData.getSchemas(); //null, schemaPattern);
145         try
146         {
147             while(rs.next())
148             {
149                 String foundSchema = rs.getString("TABLE_SCHEM");
150                 if(foundSchema.equals(schema))
151                 {
152                     return true;
153                 }
154             }
155 
156             // Especially for MySQL check the catalog
157             if(catalogExists(connection, schema))
158             {
159                 logger.debug("Found catalog with name {}. Returning true because DB is probably on MySQL", schema);
160                 return true;
161             }
162 
163             return false;
164         }
165         finally
166         {
167             rs.close();
168         }
169             }
170 
171     /**
172      * Checks via {@link DatabaseMetaData#getCatalogs()} whether or not the given catalog exists.
173      * @param connection
174      * @param catalog
175      * @return
176      * @throws SQLException
177      * @since 2.4.4
178      */
179     private static boolean catalogExists(Connection connection, String catalog) throws SQLException
180     {
181         logger.trace("catalogExists(connection={}, catalog={}) - start", connection, catalog);
182 
183         if(catalog == null)
184         {
185             throw new NullPointerException("The parameter 'catalog' must not be null");
186         }
187 
188         DatabaseMetaData metaData = connection.getMetaData();
189         ResultSet rs = metaData.getCatalogs();
190         try
191         {
192             while(rs.next())
193             {
194                 String foundCatalog = rs.getString("TABLE_CAT");
195                 if(foundCatalog.equals(catalog))
196                 {
197                     return true;
198                 }
199             }
200             return false;
201         }
202         finally
203         {
204             rs.close();
205         }
206 
207     }
208 
209     /**
210      * Checks if the given table exists.
211      * @param metaData The database meta data
212      * @param schema The schema in which the table should be searched. If <code>null</code>
213      * the schema is not used to narrow the table name.
214      * @param tableName The table name to be searched
215      * @return Returns <code>true</code> if the given table exists in the given schema.
216      * Else returns <code>false</code>.
217      * @throws SQLException
218      * @since 2.3.0
219      * @deprecated since 2.4.5 - use {@link IMetadataHandler#tableExists(DatabaseMetaData, String, String)}
220      */
221     public static boolean tableExists(DatabaseMetaData metaData, String schema,
222             String tableName)
223                     throws SQLException
224                     {
225         ResultSet tableRs = metaData.getTables(null, schema, tableName, null);
226         try
227         {
228             return tableRs.next();
229         }
230         finally
231         {
232             SQLHelper.close(tableRs);
233         }
234                     }
235 
236     /**
237      * Utility method for debugging to print all tables of the given metadata on the given stream
238      * @param metaData
239      * @param outputStream
240      * @throws SQLException
241      */
242     public static void printAllTables(DatabaseMetaData metaData, PrintStream outputStream) throws SQLException
243     {
244         ResultSet rs = metaData.getTables(null, null, null, null);
245         try
246         {
247             while (rs.next())
248             {
249                 String catalog = rs.getString("TABLE_CAT");
250                 String schema = rs.getString("TABLE_SCHEM");
251                 String table = rs.getString("TABLE_NAME");
252                 StringBuffer tableInfo = new StringBuffer();
253      			if(catalog!=null) tableInfo.append(catalog).append(".");
254      			if(schema!=null) tableInfo.append(schema).append(".");
255                 tableInfo.append(table);
256                 // Print the info
257                 outputStream.println(tableInfo);
258             }
259             outputStream.flush();
260         }
261         finally
262         {
263             SQLHelper.close(rs);
264         }
265 
266     }
267 
268     /**
269      * Returns the database and JDBC driver information as pretty formatted string
270      * @param metaData The JDBC database metadata needed to retrieve database information
271      * @return The database information as formatted string
272      */
273     public static String getDatabaseInfo(DatabaseMetaData metaData)
274     {
275         StringBuffer sb = new StringBuffer();
276         sb.append("\n");
277 
278         String dbInfo = null;
279 
280         dbInfo = new ExceptionWrapper(){
281             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
282                 return metaData.getDatabaseProductName();
283             }
284         }.executeWrappedCall(metaData);
285         sb.append("\tdatabase product name=").append(dbInfo).append("\n");
286 
287         dbInfo = new ExceptionWrapper(){
288             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
289                 return metaData.getDatabaseProductVersion();
290             }
291         }.executeWrappedCall(metaData);
292         sb.append("\tdatabase version=").append(dbInfo).append("\n");
293 
294         dbInfo = new ExceptionWrapper(){
295             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
296                 return String.valueOf(metaData.getDatabaseMajorVersion());
297             }
298         }.executeWrappedCall(metaData);
299         sb.append("\tdatabase major version=").append(dbInfo).append("\n");
300 
301         dbInfo = new ExceptionWrapper(){
302             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
303                 return String.valueOf(metaData.getDatabaseMinorVersion());
304             }
305         }.executeWrappedCall(metaData);
306         sb.append("\tdatabase minor version=").append(dbInfo).append("\n");
307 
308         dbInfo = new ExceptionWrapper(){
309             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
310                 return metaData.getDriverName();
311             }
312         }.executeWrappedCall(metaData);
313         sb.append("\tjdbc driver name=").append(dbInfo).append("\n");
314 
315         dbInfo = new ExceptionWrapper(){
316             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
317                 return metaData.getDriverVersion();
318             }
319         }.executeWrappedCall(metaData);
320         sb.append("\tjdbc driver version=").append(dbInfo).append("\n");
321 
322         dbInfo = new ExceptionWrapper(){
323             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
324                 return String.valueOf(metaData.getDriverMajorVersion());
325             }
326         }.executeWrappedCall(metaData);
327         sb.append("\tjdbc driver major version=").append(dbInfo).append("\n");
328 
329         dbInfo = new ExceptionWrapper(){
330             public String wrappedCall(DatabaseMetaData metaData) throws Exception {
331                 return String.valueOf(metaData.getDriverMinorVersion());
332             }
333         }.executeWrappedCall(metaData);
334         sb.append("\tjdbc driver minor version=").append(dbInfo).append("\n");
335 
336         return sb.toString();
337     }
338 
339     /**
340      * Prints the database and JDBC driver information to the given output stream
341      * @param metaData The JDBC database metadata needed to retrieve database information
342      * @param outputStream The stream to which the information is printed
343      * @throws SQLException
344      */
345     public static void printDatabaseInfo(DatabaseMetaData metaData, PrintStream outputStream) throws SQLException
346     {
347         String dbInfo = getDatabaseInfo(metaData);
348         try {
349             outputStream.println(dbInfo);
350         }
351         finally {
352             outputStream.flush();
353         }
354     }
355 
356     /**
357      * Detects whether or not the given metadata describes the connection to a Sybase database
358      * or not.
359      * @param metaData The metadata to be checked whether it is a Sybase connection
360      * @return <code>true</code> if and only if the given metadata belongs to a Sybase database.
361      * @throws SQLException
362      */
363     public static boolean isSybaseDb(DatabaseMetaData metaData) throws SQLException
364     {
365         String dbProductName = metaData.getDatabaseProductName();
366         boolean isSybase = (dbProductName != null && dbProductName.equals(DB_PRODUCT_SYBASE));
367         return isSybase;
368     }
369 
370 
371     /**
372      * Utility method to create a {@link Column} object from a SQL {@link ResultSet} object.
373      * 
374      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
375      * @param dataTypeFactory The factory used to lookup the {@link DataType} for this column
376      * @param datatypeWarning Whether or not a warning should be printed if the column could not
377      * be created because of an unknown datatype.
378      * @return The {@link Column} or <code>null</code> if the column could not be initialized because of an
379      * unknown datatype.
380      * @throws SQLException
381      * @throws DataTypeException
382      * @since 2.4.0
383      */
384     public static final Column createColumn(ResultSet resultSet,
385             IDataTypeFactory dataTypeFactory, boolean datatypeWarning)
386                     throws SQLException, DataTypeException
387 	{
388         ColumnMetaData columnMetadata = new ColumnMetaData(resultSet);
389         return createColumn(columnMetadata, dataTypeFactory, datatypeWarning);
390 	}
391 
392 
393     /**
394      * Utility method to create a {@link Column} object from a SQL {@link ResultSet} object.
395      * 
396      * @param columnMetadata Column metadata
397      * @param dataTypeFactory The factory used to lookup the {@link DataType} for this column
398      * @param datatypeWarning Whether or not a warning should be printed if the column could not
399      * be created because of an unknown datatype.
400      * @return The {@link Column} or <code>null</code> if the column could not be initialized because of an
401      * unknown datatype.
402      * @throws SQLException
403      * @throws DataTypeException
404      * @since 2.4.0
405      */
406     public static final Column createColumn(ColumnMetaData columnMetadata,
407             IDataTypeFactory dataTypeFactory, boolean datatypeWarning)
408                     throws SQLException, DataTypeException
409     {
410         // Convert SQL type to DataType
411         DataType dataType = columnMetadata.getDataType(dataTypeFactory);
412         if (dataType == DataType.UNKNOWN) {
413             // datatype unknown - column not created
414             if (datatypeWarning)
415                 logger.warn(columnMetadata.getTableName() + "." + columnMetadata.getColumnName() +
416                         " data type (" + columnMetadata.getSQLType() + ", '" + columnMetadata.getSqlTypeName() +
417                         "') not recognized and will be ignored. See FAQ for more information.");
418             return null;
419         } else {
420             return new Column(columnMetadata, dataType);
421         }
422     }
423     
424     /**
425      * Checks if the given <code>resultSet</code> matches the given schema and table name.
426      * The comparison is <b>case sensitive</b>.
427      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
428      * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
429      * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
430      * @param caseSensitive Whether or not the comparison should be case sensitive or not
431      * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
432      * the given schema and table parameters.
433      * @throws SQLException
434      * @since 2.4.0
435      * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
436      */
437     public static boolean matches(ResultSet resultSet,
438             String schema, String table, boolean caseSensitive)
439                     throws SQLException
440                     {
441         return matches(resultSet, null, schema, table, null, caseSensitive);
442                     }
443 
444 
445     /**
446      * Checks if the given <code>resultSet</code> matches the given schema and table name.
447      * The comparison is <b>case sensitive</b>.
448      * @param resultSet A result set produced via {@link DatabaseMetaData#getColumns(String, String, String, String)}
449      * @param catalog The name of the catalog to check. If <code>null</code> it is ignored in the comparison
450      * @param schema The name of the schema to check. If <code>null</code> it is ignored in the comparison
451      * @param table The name of the table to check. If <code>null</code> it is ignored in the comparison
452      * @param column The name of the column to check. If <code>null</code> it is ignored in the comparison
453      * @param caseSensitive Whether or not the comparison should be case sensitive or not
454      * @return <code>true</code> if the column metadata of the given <code>resultSet</code> matches
455      * the given schema and table parameters.
456      * @throws SQLException
457      * @since 2.4.0
458      * @deprecated since 2.4.4 - use {@link IMetadataHandler#matches(ResultSet, String, String, String, String, boolean)}
459      */
460     public static boolean matches(ResultSet resultSet,
461             String catalog, String schema,
462             String table, String column, boolean caseSensitive)
463                     throws SQLException
464                     {
465         String catalogName = resultSet.getString(1);
466         String schemaName = resultSet.getString(2);
467         String tableName = resultSet.getString(3);
468         String columnName = resultSet.getString(4);
469 
470         // MYSQL provides only a catalog but no schema
471         if(schema != null && schemaName == null && catalog==null && catalogName != null){
472             logger.debug("Switching catalog/schema because the are mutually null");
473             schemaName = catalogName;
474             catalogName = null;
475         }
476 
477         boolean areEqual =
478                 areEqualIgnoreNull(catalog, catalogName, caseSensitive) &&
479                 areEqualIgnoreNull(schema, schemaName, caseSensitive) &&
480                 areEqualIgnoreNull(table, tableName, caseSensitive) &&
481                 areEqualIgnoreNull(column, columnName, caseSensitive);
482         return areEqual;
483                     }
484 
485     /**
486      * Compares the given values and returns true if they are equal.
487      * If the first value is <code>null</code> or empty String it always
488      * returns <code>true</code> which is the way of ignoring <code>null</code>s
489      * for this specific case.
490      * @param value1 The first value to compare. Is ignored if null or empty String
491      * @param value2 The second value to be compared
492      * @return <code>true</code> if both values are equal or if the first value
493      * is <code>null</code> or empty string.
494      * @since 2.4.4
495      */
496     public static final boolean areEqualIgnoreNull(String value1, String value2, boolean caseSensitive)
497     {
498         if(value1==null || value1.equals(""))
499         {
500             return true;
501         }
502         else
503         {
504             if(caseSensitive && value1.equals(value2))
505             {
506                 return true;
507             }
508             else if(!caseSensitive && value1.equalsIgnoreCase(value2))
509             {
510                 return true;
511             }
512             else
513             {
514                 return false;
515             }
516         }
517     }
518 
519     /**
520      * Corrects the case of the given String according to the way in which the database stores metadata.
521      * @param databaseIdentifier A database identifier such as a table name or a schema name for
522      * which the case should be corrected.
523      * @param connection The connection used to lookup the database metadata. This is needed to determine
524      * the way in which the database stores its metadata.
525      * @return The database identifier in the correct case for the RDBMS
526      * @since 2.4.4
527      */
528     public static final String correctCase(final String databaseIdentifier, Connection connection)
529     {
530         logger.trace("correctCase(tableName={}, connection={}) - start", databaseIdentifier, connection);
531 
532         try
533         {
534             return correctCase(databaseIdentifier, connection.getMetaData());
535         }
536         catch (SQLException e)
537         {
538             throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
539         }
540     }
541 
542     /**
543      * Corrects the case of the given String according to the way in which the database stores metadata.
544      * @param databaseIdentifier A database identifier such as a table name or a schema name for
545      * which the case should be corrected.
546      * @param databaseMetaData The database metadata needed to determine the way in which the database stores
547      * its metadata.
548      * @return The database identifier in the correct case for the RDBMS
549      * @since 2.4.4
550      */
551     public static final String correctCase(final String databaseIdentifier, DatabaseMetaData databaseMetaData)
552     {
553         logger.trace("correctCase(tableName={}, databaseMetaData={}) - start", databaseIdentifier, databaseMetaData);
554 
555         if (databaseIdentifier == null) {
556             throw new NullPointerException(
557                     "The parameter 'databaseIdentifier' must not be null");
558         }
559         if (databaseMetaData == null) {
560             throw new NullPointerException(
561                     "The parameter 'databaseMetaData' must not be null");
562         }
563 
564         try {
565             String resultTableName = databaseIdentifier;
566             String dbIdentifierQuoteString = databaseMetaData.getIdentifierQuoteString();
567             if(!isEscaped(databaseIdentifier, dbIdentifierQuoteString)){
568                 if(databaseMetaData.storesLowerCaseIdentifiers())
569                 {
570                     resultTableName = databaseIdentifier.toLowerCase(Locale.ENGLISH);
571                 }
572                 else if(databaseMetaData.storesUpperCaseIdentifiers())
573                 {
574                     resultTableName = databaseIdentifier.toUpperCase(Locale.ENGLISH);
575                 }
576                 else
577                 {
578                     logger.debug("Database does not store upperCase or lowerCase identifiers. " +
579                             "Will not correct case of the table names.");
580                 }
581             }
582             else
583             {
584                 if(logger.isDebugEnabled())
585                     logger.debug("The tableName '{}' is escaped. Will not correct case.", databaseIdentifier);
586                 }
587             return resultTableName;
588         }
589         catch (SQLException e)
590         {
591             throw new DatabaseUnitRuntimeException("Exception while trying to access database metadata", e);
592         }
593     }
594 
595     /**
596      * Checks whether two given values are unequal and if so print a log message (level DEBUG)
597      * @param oldValue The old value of a property
598      * @param newValue The new value of a property
599      * @param message The message to be logged
600      * @param source The class which invokes this method - used for enriching the log message
601      * @since 2.4.4
602      */
603     public static final void logInfoIfValueChanged(String oldValue, String newValue, String message, Class source)
604     {
605         if(logger.isInfoEnabled())
606         {
607             if(oldValue != null && !oldValue.equals(newValue))
608                 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
609             }
610         }
611 
612     /**
613      * Checks whether two given values are unequal and if so print a log message (level DEBUG)
614      * @param oldValue The old value of a property
615      * @param newValue The new value of a property
616      * @param message The message to be logged
617      * @param source The class which invokes this method - used for enriching the log message
618      * @since 2.4.8
619      */
620     public static final void logDebugIfValueChanged(String oldValue, String newValue, String message, Class source)
621     {
622         if (logger.isDebugEnabled())
623         {
624             if (oldValue != null && !oldValue.equals(newValue))
625                 logger.debug("{}. {} oldValue={} newValue={}", new Object[] {source, message, oldValue, newValue});
626             }
627         }
628 
629     /**
630      * @param tableName
631      * @param dbIdentifierQuoteString
632      * @return
633      * @since 2.4.4
634      */
635     private static final boolean isEscaped(String tableName, String dbIdentifierQuoteString)
636     {
637         logger.trace("isEscaped(tableName={}, dbIdentifierQuoteString={}) - start", tableName, dbIdentifierQuoteString);
638 
639         if (dbIdentifierQuoteString == null) {
640             throw new NullPointerException(
641                     "The parameter 'dbIdentifierQuoteString' must not be null");
642         }
643         boolean isEscaped = tableName!=null && (tableName.startsWith(dbIdentifierQuoteString));
644         if(logger.isDebugEnabled())
645             logger.debug("isEscaped returns '{}' for tableName={} (dbIdentifierQuoteString={})",
646                     new Object[]{Boolean.valueOf(isEscaped), tableName, dbIdentifierQuoteString} );
647         return isEscaped;
648     }
649 
650 
651     /**
652      * Performs a method invocation and catches all exceptions that occur during the invocation.
653      * Utility which works similar to a closure, just a bit less elegant.
654      * @author gommma (gommma AT users.sourceforge.net)
655      * @author Last changed by: $Author$
656      * @version $Revision$ $Date$
657      * @since 2.4.6
658      */
659     static abstract class ExceptionWrapper{
660 
661         public static final String NOT_AVAILABLE_TEXT = "<not available>";
662 
663         /**
664          * Default constructor
665          */
666         public ExceptionWrapper()
667         {
668         }
669 
670         /**
671          * Executes the call and catches all exception that might occur.
672          * @param metaData
673          * @return The result of the call
674          */
675         public final String executeWrappedCall(DatabaseMetaData metaData) {
676             try{
677                 String result = wrappedCall(metaData);
678                 return result;
679             }
680             catch(Exception e){
681                 logger.trace("Problem retrieving DB information via DatabaseMetaData", e);
682                 return NOT_AVAILABLE_TEXT;
683             }
684         }
685         /**
686          * Calls the method that might throw an exception to be handled
687          * @param metaData
688          * @return The result of the call as human readable string
689          * @throws Exception Any exception that might occur during the method invocation
690          */
691         public abstract String wrappedCall(DatabaseMetaData metaData) throws Exception;
692     }
693 
694 }