View Javadoc
1   /*
2    *    Copyright 2006-2026 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.generator.internal.db;
17  
18  import static org.mybatis.generator.internal.util.StringUtility.composeFullyQualifiedTableName;
19  import static org.mybatis.generator.internal.util.StringUtility.isTrue;
20  import static org.mybatis.generator.internal.util.StringUtility.stringContainsSQLWildcard;
21  import static org.mybatis.generator.internal.util.StringUtility.stringContainsSpace;
22  import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
23  import static org.mybatis.generator.internal.util.messages.Messages.getString;
24  
25  import java.sql.DatabaseMetaData;
26  import java.sql.ResultSet;
27  import java.sql.ResultSetMetaData;
28  import java.sql.SQLException;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.StringTokenizer;
36  import java.util.TreeMap;
37  import java.util.regex.Matcher;
38  
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  import org.mybatis.generator.api.FullyQualifiedTable;
42  import org.mybatis.generator.api.IntrospectedColumn;
43  import org.mybatis.generator.api.IntrospectedTable;
44  import org.mybatis.generator.api.JavaTypeResolver;
45  import org.mybatis.generator.api.KnownRuntime;
46  import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
47  import org.mybatis.generator.api.dom.java.JavaReservedWords;
48  import org.mybatis.generator.config.ColumnOverride;
49  import org.mybatis.generator.config.Context;
50  import org.mybatis.generator.config.GeneratedKey;
51  import org.mybatis.generator.config.PropertyRegistry;
52  import org.mybatis.generator.config.TableConfiguration;
53  import org.mybatis.generator.internal.ObjectFactory;
54  import org.mybatis.generator.internal.PluginAggregator;
55  import org.mybatis.generator.internal.util.JavaBeansUtil;
56  
57  public class DatabaseIntrospector {
58      private final DatabaseMetaData databaseMetaData;
59      private final JavaTypeResolver javaTypeResolver;
60      private final List<String> warnings = new ArrayList<>();
61      private final Context context;
62      private final Log logger;
63  
64      public DatabaseIntrospector(Context context, DatabaseMetaData databaseMetaData,
65                                  JavaTypeResolver javaTypeResolver) {
66          this.context = context;
67          this.databaseMetaData = databaseMetaData;
68          this.javaTypeResolver = javaTypeResolver;
69          logger = LogFactory.getLog(getClass());
70      }
71  
72      public List<String> getWarnings() {
73          return Collections.unmodifiableList(warnings);
74      }
75  
76      private void calculatePrimaryKey(FullyQualifiedTable table, IntrospectedTable introspectedTable) {
77          try (ResultSet rs = databaseMetaData.getPrimaryKeys(
78                  table.getIntrospectedCatalog().orElse(null),
79                  table.getIntrospectedSchema().orElse(null),
80                  table.getIntrospectedTableName())) {
81              // keep primary columns in key sequence order
82              Map<Short, String> keyColumns = new TreeMap<>();
83              while (rs.next()) {
84                  String columnName = rs.getString("COLUMN_NAME"); //$NON-NLS-1$
85                  short keySeq = rs.getShort("KEY_SEQ"); //$NON-NLS-1$
86                  keyColumns.put(keySeq, columnName);
87              }
88  
89              for (String columnName : keyColumns.values()) {
90                  introspectedTable.addPrimaryKeyColumn(columnName);
91              }
92          } catch (SQLException e) {
93              warnings.add(getString("Warning.15")); //$NON-NLS-1$
94          }
95      }
96  
97      private void reportIntrospectionWarnings(IntrospectedTable introspectedTable,
98                                               TableConfiguration tableConfiguration, FullyQualifiedTable table) {
99          // make sure that every column listed in column overrides
100         // actually exists in the table
101         for (ColumnOverride columnOverride : tableConfiguration.getColumnOverrides()) {
102             if (introspectedTable.getColumn(columnOverride.getColumnName()).isEmpty()) {
103                 warnings.add(getString("Warning.3", columnOverride.getColumnName(), table.toString()));//$NON-NLS-1$
104             }
105         }
106 
107         // make sure that every column listed in ignored columns
108         // actually exists in the table
109         for (String string : tableConfiguration.getIgnoredColumnsInError()) {
110             warnings.add(getString("Warning.4", string, table.toString())); //$NON-NLS-1$
111         }
112 
113         tableConfiguration.getGeneratedKey().ifPresent(generatedKey -> {
114             if (introspectedTable.getColumn(generatedKey.getColumn()).isEmpty()) {
115                 if (generatedKey.isIdentity()) {
116                     warnings.add(getString("Warning.5", generatedKey.getColumn(), table.toString())); //$NON-NLS-1$
117                 } else {
118                     warnings.add(getString("Warning.6", generatedKey.getColumn(), table.toString())); //$NON-NLS-1$
119                 }
120             }
121         });
122 
123         for (IntrospectedColumn ic : introspectedTable.getAllColumns()) {
124             if (JavaReservedWords.containsWord(ic.getJavaProperty())) {
125                 warnings.add(getString("Warning.26", ic.getActualColumnName(), table.toString())); //$NON-NLS-1$
126             }
127         }
128     }
129 
130     /**
131      * Returns a List of IntrospectedTable elements that matches the specified table configuration.
132      *
133      * @param tc
134      *            the table configuration
135      * @return a list of introspected tables
136      * @throws SQLException
137      *             if any errors in introspection
138      */
139     public List<IntrospectedTable> introspectTables(TableConfiguration tc, KnownRuntime knownRuntime,
140                                                     PluginAggregator pluginAggregator) throws SQLException {
141         // get the raw columns from the DB
142         Map<ActualTableName, List<IntrospectedColumn>> columns = getColumns(tc);
143 
144         if (columns.isEmpty()) {
145             String formattedCatalog = tc.getCatalog() == null ? "<null>" : "'" + tc.getCatalog() + "'";
146             String formattedSchema = tc.getSchema() == null ? "<null>" : "'" + tc.getSchema() + "'";
147             String formattedTableName = "'" + tc.getTableName() + "'";
148             warnings.add(getString("Warning.19", formattedCatalog, formattedSchema, formattedTableName)); //$NON-NLS-1$
149             return Collections.emptyList();
150         }
151 
152         removeIgnoredColumns(tc, columns);
153         calculateExtraColumnInformation(tc, columns);
154         applyColumnOverrides(tc, columns);
155         calculateIdentityColumns(tc, columns);
156 
157         List<IntrospectedTable> introspectedTables =
158                 calculateIntrospectedTables(tc, columns, knownRuntime, pluginAggregator);
159 
160         // now introspectedTables has all the columns from all the
161         // tables in the configuration. Do some validation...
162 
163         Iterator<IntrospectedTable> iter = introspectedTables.iterator();
164         while (iter.hasNext()) {
165             IntrospectedTable introspectedTable = iter.next();
166 
167             if (!introspectedTable.hasAnyColumns()) {
168                 // add a warning that the table has no columns, remove from the
169                 // list
170                 String warning =
171                         getString("Warning.1", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$
172                 warnings.add(warning);
173                 iter.remove();
174             } else if (!introspectedTable.hasPrimaryKeyColumns() && !introspectedTable.hasBaseColumns()) {
175                 // add a warning that the table has only BLOB columns, remove from
176                 // the list
177                 String warning =
178                         getString("Warning.18", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$
179                 warnings.add(warning);
180                 iter.remove();
181             } else {
182                 // now make sure that all columns called out in the
183                 // configuration
184                 // actually exist
185                 reportIntrospectionWarnings(introspectedTable, tc, introspectedTable.getFullyQualifiedTable());
186             }
187         }
188 
189         return introspectedTables;
190     }
191 
192     private void removeIgnoredColumns(TableConfiguration tc, Map<ActualTableName, List<IntrospectedColumn>> columns) {
193         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
194             Iterator<IntrospectedColumn> tableColumns = entry.getValue().iterator();
195             while (tableColumns.hasNext()) {
196                 IntrospectedColumn introspectedColumn = tableColumns.next();
197                 if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) {
198                     tableColumns.remove();
199                     if (logger.isDebugEnabled()) {
200                         logger.debug(getString("Tracing.3", //$NON-NLS-1$
201                                 introspectedColumn.getActualColumnName(), entry.getKey().toString()));
202                     }
203                 }
204             }
205         }
206     }
207 
208     private void calculateExtraColumnInformation(TableConfiguration tc, Map<ActualTableName,
209             List<IntrospectedColumn>> columns) {
210         StringBuilder sb = new StringBuilder();
211 
212         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
213             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
214                 String calculatedColumnName = tc.getColumnRenamingRule().map(rr -> {
215                     Matcher matcher = rr.pattern().matcher(introspectedColumn.getActualColumnName());
216                     return matcher.replaceAll(rr.replaceString());
217                 }).orElseGet(introspectedColumn::getActualColumnName);
218 
219                 if (isTrue(tc.getProperty(PropertyRegistry.TABLE_USE_ACTUAL_COLUMN_NAMES))) {
220                     introspectedColumn.setJavaProperty(JavaBeansUtil.getValidPropertyName(calculatedColumnName));
221                 } else if (isTrue(tc.getProperty(PropertyRegistry.TABLE_USE_COMPOUND_PROPERTY_NAMES))) {
222                     sb.setLength(0);
223                     sb.append(calculatedColumnName);
224                     introspectedColumn.getRemarks().ifPresent(r -> {
225                         sb.append('_');
226                         sb.append(JavaBeansUtil.getCamelCaseString(r, true));
227                     });
228                     introspectedColumn.setJavaProperty(JavaBeansUtil.getValidPropertyName(sb.toString()));
229                 } else {
230                     introspectedColumn.setJavaProperty(
231                             JavaBeansUtil.getCamelCaseString(calculatedColumnName, false));
232                 }
233 
234                 javaTypeResolver.calculateTypeInformation(introspectedColumn).ifPresentOrElse(ti -> {
235                     introspectedColumn.setFullyQualifiedJavaType(ti.fullyQualifiedJavaType());
236                     introspectedColumn.setJdbcTypeName(ti.jdbcTypeName());
237                 }, () -> {
238                     // type cannot be resolved. Check for ignored or overridden
239                     if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) {
240                         return;
241                     }
242 
243                     // if overridden, and a java type is configured, then we can ignore
244                     if (tc.getColumnOverride(introspectedColumn.getActualColumnName())
245                             .flatMap(ColumnOverride::getJavaType)
246                             .isPresent()) {
247                         return;
248                     }
249 
250                     // if the type is not supported, then we'll report a warning
251                     introspectedColumn.setFullyQualifiedJavaType(FullyQualifiedJavaType.getObjectInstance());
252                     introspectedColumn.setJdbcTypeName("OTHER"); //$NON-NLS-1$
253 
254                     String warning = getString("Warning.14", //$NON-NLS-1$
255                             Integer.toString(introspectedColumn.getJdbcType()),
256                             entry.getKey().toString(),
257                             introspectedColumn.getActualColumnName());
258 
259                     warnings.add(warning);
260                 });
261 
262                 if (context.autoDelimitKeywords()
263                         && SqlReservedWords.containsWord(introspectedColumn.getActualColumnName())) {
264                     introspectedColumn.setColumnNameDelimited(true);
265                 }
266 
267                 if (tc.isAllColumnDelimitingEnabled()) {
268                     introspectedColumn.setColumnNameDelimited(true);
269                 }
270             }
271         }
272     }
273 
274     private void calculateIdentityColumns(TableConfiguration tc,
275                                           Map<ActualTableName, List<IntrospectedColumn>> columns) {
276         tc.getGeneratedKey().ifPresent(gk -> columns.values().stream()
277                 .flatMap(List::stream)
278                 .filter(introspectedColumn -> isMatchedColumn(introspectedColumn, gk))
279                 .forEach(introspectedColumn -> {
280                     if (gk.isIdentity() || gk.isJdbcStandard()) {
281                         introspectedColumn.setIdentity(true);
282                         introspectedColumn.setSequenceColumn(false);
283                     } else {
284                         introspectedColumn.setIdentity(false);
285                         introspectedColumn.setSequenceColumn(true);
286                     }
287                 }));
288     }
289 
290     private boolean isMatchedColumn(IntrospectedColumn introspectedColumn, GeneratedKey gk) {
291         if (introspectedColumn.isColumnNameDelimited()) {
292             return introspectedColumn.getActualColumnName().equals(gk.getColumn());
293         } else {
294             return introspectedColumn.getActualColumnName().equalsIgnoreCase(gk.getColumn());
295         }
296     }
297 
298     private void applyColumnOverrides(TableConfiguration tc, Map<ActualTableName, List<IntrospectedColumn>> columns) {
299         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
300             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
301                 tc.getColumnOverride(introspectedColumn.getActualColumnName()).ifPresent(columnOverride -> {
302                     if (logger.isDebugEnabled()) {
303                         logger.debug(getString("Tracing.4", //$NON-NLS-1$
304                                 introspectedColumn.getActualColumnName(), entry.getKey().toString()));
305                     }
306 
307                     columnOverride.getJavaProperty().ifPresent(introspectedColumn::setJavaProperty);
308                     columnOverride.getJavaType().ifPresent(
309                             jt -> introspectedColumn.setFullyQualifiedJavaType(new FullyQualifiedJavaType(jt)));
310                     columnOverride.getJdbcType().ifPresent(introspectedColumn::setJdbcTypeName);
311                     columnOverride.getTypeHandler().ifPresent(introspectedColumn::setTypeHandler);
312 
313                     if (columnOverride.isColumnNameDelimited()) {
314                         introspectedColumn.setColumnNameDelimited(true);
315                     }
316 
317                     introspectedColumn.setGeneratedAlways(columnOverride.isGeneratedAlways());
318 
319                     introspectedColumn.setProperties(columnOverride.getProperties());
320                 });
321             }
322         }
323     }
324 
325     private Map<ActualTableName, List<IntrospectedColumn>> getColumns(TableConfiguration tc) throws SQLException {
326         String localCatalog;
327         String localSchema;
328         String localTableName;
329 
330         boolean delimitIdentifiers = tc.isDelimitIdentifiers()
331                 || stringContainsSpace(tc.getCatalog())
332                 || stringContainsSpace(tc.getSchema())
333                 || stringContainsSpace(tc.getTableName());
334 
335         if (delimitIdentifiers) {
336             localCatalog = tc.getCatalog();
337             localSchema = tc.getSchema();
338             localTableName = tc.getTableName();
339         } else if (databaseMetaData.storesLowerCaseIdentifiers()) {
340             localCatalog = tc.getCatalog() == null ? null : tc.getCatalog().toLowerCase();
341             localSchema = tc.getSchema() == null ? null : tc.getSchema().toLowerCase();
342             localTableName = tc.getTableName().toLowerCase();
343         } else if (databaseMetaData.storesUpperCaseIdentifiers()) {
344             localCatalog = tc.getCatalog() == null ? null : tc.getCatalog().toUpperCase();
345             localSchema = tc.getSchema() == null ? null : tc.getSchema().toUpperCase();
346             localTableName = tc.getTableName().toUpperCase();
347         } else {
348             localCatalog = tc.getCatalog();
349             localSchema = tc.getSchema();
350             localTableName = tc.getTableName();
351         }
352 
353         if (tc.isWildcardEscapingEnabled()) {
354             String escapeString = databaseMetaData.getSearchStringEscape();
355 
356             if (localSchema != null) {
357                 localSchema = escapeName(localSchema, escapeString);
358             }
359 
360             localTableName = escapeName(localTableName, escapeString);
361         }
362 
363         Map<ActualTableName, List<IntrospectedColumn>> answer = new HashMap<>();
364 
365         if (logger.isDebugEnabled()) {
366             String fullTableName = composeFullyQualifiedTableName(localCatalog, localSchema,
367                     localTableName, '.');
368             logger.debug(getString("Tracing.1", fullTableName)); //$NON-NLS-1$
369         }
370 
371         try (ResultSet rs
372                      = databaseMetaData.getColumns(localCatalog, localSchema, localTableName, "%")) { //$NON-NLS-1$
373             boolean supportsIsAutoIncrement = false;
374             boolean supportsIsGeneratedColumn = false;
375             ResultSetMetaData rsmd = rs.getMetaData();
376             int colCount = rsmd.getColumnCount();
377             for (int i = 1; i <= colCount; i++) {
378                 if ("IS_AUTOINCREMENT".equals(rsmd.getColumnName(i))) { //$NON-NLS-1$
379                     supportsIsAutoIncrement = true;
380                 }
381                 if ("IS_GENERATEDCOLUMN".equals(rsmd.getColumnName(i))) { //$NON-NLS-1$
382                     supportsIsGeneratedColumn = true;
383                 }
384             }
385 
386             while (rs.next()) {
387                 IntrospectedColumn introspectedColumn = ObjectFactory.createIntrospectedColumn(context);
388 
389                 introspectedColumn.setTableAlias(tc.getAlias());
390                 introspectedColumn.setJdbcType(rs.getInt("DATA_TYPE")); //$NON-NLS-1$
391                 introspectedColumn.setActualTypeName(rs.getString("TYPE_NAME")); //$NON-NLS-1$
392                 introspectedColumn.setLength(rs.getInt("COLUMN_SIZE")); //$NON-NLS-1$
393                 introspectedColumn.setActualColumnName(rs.getString("COLUMN_NAME")); //$NON-NLS-1$
394                 introspectedColumn
395                         .setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable); //$NON-NLS-1$
396                 introspectedColumn.setScale(rs.getInt("DECIMAL_DIGITS")); //$NON-NLS-1$
397                 introspectedColumn.setRemarks(rs.getString("REMARKS")); //$NON-NLS-1$
398                 introspectedColumn.setDefaultValue(rs.getString("COLUMN_DEF")); //$NON-NLS-1$
399 
400                 if (supportsIsAutoIncrement) {
401                     introspectedColumn.setAutoIncrement(
402                             "YES".equals(rs.getString("IS_AUTOINCREMENT"))); //$NON-NLS-1$ //$NON-NLS-2$
403                 }
404 
405                 if (supportsIsGeneratedColumn) {
406                     introspectedColumn.setGeneratedColumn(
407                             "YES".equals(rs.getString("IS_GENERATEDCOLUMN"))); //$NON-NLS-1$ //$NON-NLS-2$
408                 }
409 
410                 ActualTableName atn = new ActualTableName(
411                         rs.getString("TABLE_CAT"), //$NON-NLS-1$
412                         rs.getString("TABLE_SCHEM"), //$NON-NLS-1$
413                         rs.getString("TABLE_NAME")); //$NON-NLS-1$
414 
415                 List<IntrospectedColumn> columns = answer.computeIfAbsent(atn, k -> new ArrayList<>());
416 
417                 columns.add(introspectedColumn);
418 
419                 if (logger.isDebugEnabled()) {
420                     logger.debug(getString(
421                             "Tracing.2", //$NON-NLS-1$
422                             introspectedColumn.getActualColumnName(),
423                             Integer.toString(introspectedColumn.getJdbcType()),
424                             atn.toString()));
425                 }
426             }
427         }
428 
429         if (answer.size() > 1
430                 && !stringContainsSQLWildcard(localSchema)
431                 && !stringContainsSQLWildcard(localTableName)) {
432             // issue a warning if there is more than one table and
433             // no wildcards were used
434             ActualTableName inputAtn = new ActualTableName(tc.getCatalog(), tc.getSchema(), tc.getTableName());
435 
436             StringBuilder sb = new StringBuilder();
437             boolean comma = false;
438             for (ActualTableName atn : answer.keySet()) {
439                 if (comma) {
440                     sb.append(',');
441                 } else {
442                     comma = true;
443                 }
444                 sb.append(atn);
445             }
446 
447             warnings.add(getString("Warning.25", inputAtn.toString(), sb.toString())); //$NON-NLS-1$
448         }
449 
450         return answer;
451     }
452 
453     private String escapeName(String localName, String escapeString) {
454         StringTokenizer st = new StringTokenizer(localName, "_%", true); //$NON-NLS-1$
455         StringBuilder sb = new StringBuilder();
456         while (st.hasMoreTokens()) {
457             String token = st.nextToken();
458             if (token.equals("_") || token.equals("%")) { //$NON-NLS-1$ //$NON-NLS-2$
459                 sb.append(escapeString);
460             }
461             sb.append(token);
462         }
463         return sb.toString();
464     }
465 
466     private List<IntrospectedTable> calculateIntrospectedTables(TableConfiguration tc, Map<ActualTableName,
467             List<IntrospectedColumn>> columns, KnownRuntime knownRuntime, PluginAggregator pluginAggregator) {
468         boolean delimitIdentifiers = tc.isDelimitIdentifiers()
469                 || stringContainsSpace(tc.getCatalog())
470                 || stringContainsSpace(tc.getSchema())
471                 || stringContainsSpace(tc.getTableName());
472 
473         List<IntrospectedTable> answer = new ArrayList<>();
474 
475         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
476             ActualTableName atn = entry.getKey();
477 
478             // we only use the returned catalog and schema if something was
479             // actually
480             // specified on the table configuration. If something was returned
481             // from the DB for these fields, but nothing was specified on the
482             // table
483             // configuration, then some sort of DB default is being returned,
484             // and we don't want that in our SQL
485             FullyQualifiedTable table = new FullyQualifiedTable.Builder()
486                     .withIntrospectedCatalog(stringHasValue(tc.getCatalog()) ? atn.getCatalog() : null)
487                     .withIntrospectedSchema(stringHasValue(tc.getSchema()) ? atn.getSchema() : null)
488                     .withIntrospectedTableName(atn.getTableName())
489                     .withDomainObjectName(tc.getDomainObjectName())
490                     .withAlias(tc.getAlias())
491                     .withIgnoreQualifiersAtRuntime(
492                             isTrue(tc.getProperty(PropertyRegistry.TABLE_IGNORE_QUALIFIERS_AT_RUNTIME)))
493                     .withRuntimeCatalog(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_CATALOG))
494                     .withRuntimeSchema(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_SCHEMA))
495                     .withRuntimeTableName(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_TABLE_NAME))
496                     .withDelimitIdentifiers(delimitIdentifiers)
497                     .withDomainObjectRenamingRule(tc.getDomainObjectRenamingRule())
498                     .withContext(context)
499                     .build();
500 
501             IntrospectedTable introspectedTable = new IntrospectedTable.Builder()
502                     .withTableConfiguration(tc)
503                     .withFullyQualifiedTable(table)
504                     .withContext(context)
505                     .withKnownRuntime(knownRuntime)
506                     .withPluginAggregator(pluginAggregator)
507                     .build();
508 
509             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
510                 introspectedTable.addColumn(introspectedColumn);
511             }
512 
513             calculatePrimaryKey(table, introspectedTable);
514 
515             enhanceIntrospectedTable(introspectedTable);
516 
517             answer.add(introspectedTable);
518         }
519 
520         return answer;
521     }
522 
523     /**
524      * Calls database metadata to retrieve extra information about the table
525      * such as remarks associated with the table and the type.
526      *
527      * <p>If there is any error, we just add a warning and continue.
528      *
529      * @param introspectedTable the introspected table to enhance
530      */
531     private void enhanceIntrospectedTable(IntrospectedTable introspectedTable) {
532         FullyQualifiedTable fqt = introspectedTable.getFullyQualifiedTable();
533         try (ResultSet rs = databaseMetaData.getTables(fqt.getIntrospectedCatalog().orElse(null),
534                 fqt.getIntrospectedSchema().orElse(null),
535                 fqt.getIntrospectedTableName(), null)) {
536 
537             if (rs.next()) {
538                 String remarks = rs.getString("REMARKS"); //$NON-NLS-1$
539                 String tableType = rs.getString("TABLE_TYPE"); //$NON-NLS-1$
540                 introspectedTable.setRemarks(remarks);
541                 introspectedTable.setTableType(tableType);
542             }
543         } catch (SQLException e) {
544             warnings.add(getString("Warning.27", e.getMessage())); //$NON-NLS-1$
545         }
546     }
547 }