View Javadoc
1   /**
2    *    Copyright 2006-2015 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    *       http://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.JavaBeansUtil.getCamelCaseString;
19  import static org.mybatis.generator.internal.util.JavaBeansUtil.getValidPropertyName;
20  import static org.mybatis.generator.internal.util.StringUtility.composeFullyQualifiedTableName;
21  import static org.mybatis.generator.internal.util.StringUtility.isTrue;
22  import static org.mybatis.generator.internal.util.StringUtility.stringContainsSQLWildcard;
23  import static org.mybatis.generator.internal.util.StringUtility.stringContainsSpace;
24  import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
25  import static org.mybatis.generator.internal.util.messages.Messages.getString;
26  
27  import java.sql.DatabaseMetaData;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.util.ArrayList;
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  import java.util.regex.Pattern;
39  
40  import org.mybatis.generator.api.FullyQualifiedTable;
41  import org.mybatis.generator.api.IntrospectedColumn;
42  import org.mybatis.generator.api.IntrospectedTable;
43  import org.mybatis.generator.api.JavaTypeResolver;
44  import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
45  import org.mybatis.generator.api.dom.java.JavaReservedWords;
46  import org.mybatis.generator.config.ColumnOverride;
47  import org.mybatis.generator.config.Context;
48  import org.mybatis.generator.config.GeneratedKey;
49  import org.mybatis.generator.config.PropertyRegistry;
50  import org.mybatis.generator.config.TableConfiguration;
51  import org.mybatis.generator.internal.ObjectFactory;
52  import org.mybatis.generator.logging.Log;
53  import org.mybatis.generator.logging.LogFactory;
54  
55  /**
56   * 
57   * @author Jeff Butler
58   */
59  public class DatabaseIntrospector {
60  
61      private DatabaseMetaData databaseMetaData;
62      private JavaTypeResolver javaTypeResolver;
63      private List<String> warnings;
64      private Context context;
65      private Log logger;
66  
67      public DatabaseIntrospector(Context context,
68              DatabaseMetaData databaseMetaData,
69              JavaTypeResolver javaTypeResolver, List<String> warnings) {
70          super();
71          this.context = context;
72          this.databaseMetaData = databaseMetaData;
73          this.javaTypeResolver = javaTypeResolver;
74          this.warnings = warnings;
75          logger = LogFactory.getLog(getClass());
76      }
77  
78      private void calculatePrimaryKey(FullyQualifiedTable table,
79              IntrospectedTable introspectedTable) {
80          ResultSet rs = null;
81  
82          try {
83              rs = databaseMetaData.getPrimaryKeys(
84                      table.getIntrospectedCatalog(), table
85                              .getIntrospectedSchema(), table
86                              .getIntrospectedTableName());
87          } catch (SQLException e) {
88              closeResultSet(rs);
89              warnings.add(getString("Warning.15")); //$NON-NLS-1$
90              return;
91          }
92  
93          try {
94              // keep primary columns in key sequence order
95              Map<Short, String> keyColumns = new TreeMap<Short, String>();
96              while (rs.next()) {
97                  String columnName = rs.getString("COLUMN_NAME"); //$NON-NLS-1$
98                  short keySeq = rs.getShort("KEY_SEQ"); //$NON-NLS-1$
99                  keyColumns.put(keySeq, columnName);
100             }
101             
102             for (String columnName : keyColumns.values()) {
103                 introspectedTable.addPrimaryKeyColumn(columnName);
104             }
105         } catch (SQLException e) {
106             // ignore the primary key if there's any error
107         } finally {
108             closeResultSet(rs);
109         }
110     }
111 
112     private void closeResultSet(ResultSet rs) {
113         if (rs != null) {
114             try {
115                 rs.close();
116             } catch (SQLException e) {
117                 // ignore
118                 ;
119             }
120         }
121     }
122 
123     private void reportIntrospectionWarnings(
124             IntrospectedTable introspectedTable,
125             TableConfiguration tableConfiguration, FullyQualifiedTable table) {
126         // make sure that every column listed in column overrides
127         // actually exists in the table
128         for (ColumnOverride columnOverride : tableConfiguration
129                 .getColumnOverrides()) {
130             if (introspectedTable.getColumn(columnOverride.getColumnName()) == null) {
131                 warnings.add(getString("Warning.3", //$NON-NLS-1$
132                         columnOverride.getColumnName(), table.toString()));
133             }
134         }
135 
136         // make sure that every column listed in ignored columns
137         // actually exists in the table
138         for (String string : tableConfiguration.getIgnoredColumnsInError()) {
139             warnings.add(getString("Warning.4", //$NON-NLS-1$
140                     string, table.toString()));
141         }
142 
143         GeneratedKey generatedKey = tableConfiguration.getGeneratedKey();
144         if (generatedKey != null
145                 && introspectedTable.getColumn(generatedKey.getColumn()) == null) {
146             if (generatedKey.isIdentity()) {
147                 warnings.add(getString("Warning.5", //$NON-NLS-1$
148                         generatedKey.getColumn(), table.toString()));
149             } else {
150                 warnings.add(getString("Warning.6", //$NON-NLS-1$
151                         generatedKey.getColumn(), table.toString()));
152             }
153         }
154         
155         for (IntrospectedColumn ic : introspectedTable.getAllColumns()) {
156             if (JavaReservedWords.containsWord(ic.getJavaProperty())) {
157                 warnings.add(getString("Warning.26", //$NON-NLS-1$
158                         ic.getActualColumnName(), table.toString()));
159             }
160         }
161     }
162 
163     /**
164      * Returns a List<IntrospectedTable> that matches the specified table
165      * configuration.
166      * 
167      * @param tc
168      * @return a list of introspected tables
169      * @throws SQLException
170      */
171     public List<IntrospectedTable> introspectTables(TableConfiguration tc)
172             throws SQLException {
173 
174         // get the raw columns from the DB
175         Map<ActualTableName, List<IntrospectedColumn>> columns = getColumns(tc);
176 
177         if (columns.isEmpty()) {
178             warnings.add(getString("Warning.19", tc.getCatalog(), //$NON-NLS-1$
179                     tc.getSchema(), tc.getTableName()));
180             return null;
181         }
182 
183         removeIgnoredColumns(tc, columns);
184         calculateExtraColumnInformation(tc, columns);
185         applyColumnOverrides(tc, columns);
186         calculateIdentityColumns(tc, columns);
187 
188         List<IntrospectedTable> introspectedTables = calculateIntrospectedTables(
189                 tc, columns);
190 
191         // now introspectedTables has all the columns from all the
192         // tables in the configuration. Do some validation...
193 
194         Iterator<IntrospectedTable> iter = introspectedTables.iterator();
195         while (iter.hasNext()) {
196             IntrospectedTable introspectedTable = iter.next();
197 
198             if (!introspectedTable.hasAnyColumns()) {
199                 // add warning that the table has no columns, remove from the
200                 // list
201                 String warning = getString(
202                                 "Warning.1", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$
203                 warnings.add(warning);
204                 iter.remove();
205             } else if (!introspectedTable.hasPrimaryKeyColumns()
206                     && !introspectedTable.hasBaseColumns()) {
207                 // add warning that the table has only BLOB columns, remove from
208                 // the list
209                 String warning = getString(
210                                 "Warning.18", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$ 
211                 warnings.add(warning);
212                 iter.remove();
213             } else {
214                 // now make sure that all columns called out in the
215                 // configuration
216                 // actually exist
217                 reportIntrospectionWarnings(introspectedTable, tc,
218                         introspectedTable.getFullyQualifiedTable());
219             }
220         }
221 
222         return introspectedTables;
223     }
224 
225     /**
226      * @param tc
227      * @param columns
228      */
229     private void removeIgnoredColumns(TableConfiguration tc,
230             Map<ActualTableName, List<IntrospectedColumn>> columns) {
231         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
232                 .entrySet()) {
233             Iterator<IntrospectedColumn> tableColumns = (entry.getValue())
234                     .iterator();
235             while (tableColumns.hasNext()) {
236                 IntrospectedColumn introspectedColumn = tableColumns.next();
237                 if (tc
238                         .isColumnIgnored(introspectedColumn
239                                 .getActualColumnName())) {
240                     tableColumns.remove();
241                     if (logger.isDebugEnabled()) {
242                         logger.debug(getString("Tracing.3", //$NON-NLS-1$
243                                 introspectedColumn.getActualColumnName(), entry
244                                         .getKey().toString()));
245                     }
246                 }
247             }
248         }
249     }
250 
251     private void calculateExtraColumnInformation(TableConfiguration tc,
252             Map<ActualTableName, List<IntrospectedColumn>> columns) {
253         StringBuilder sb = new StringBuilder();
254         Pattern pattern = null;
255         String replaceString = null;
256         if (tc.getColumnRenamingRule() != null) {
257             pattern = Pattern.compile(tc.getColumnRenamingRule()
258                     .getSearchString());
259             replaceString = tc.getColumnRenamingRule().getReplaceString();
260             replaceString = replaceString == null ? "" : replaceString; //$NON-NLS-1$
261         }
262 
263         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
264                 .entrySet()) {
265             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
266                 String calculatedColumnName;
267                 if (pattern == null) {
268                     calculatedColumnName = introspectedColumn
269                             .getActualColumnName();
270                 } else {
271                     Matcher matcher = pattern.matcher(introspectedColumn
272                             .getActualColumnName());
273                     calculatedColumnName = matcher.replaceAll(replaceString);
274                 }
275 
276                 if (isTrue(tc
277                         .getProperty(PropertyRegistry.TABLE_USE_ACTUAL_COLUMN_NAMES))) {
278                     introspectedColumn.setJavaProperty(
279                             getValidPropertyName(calculatedColumnName));
280                 } else if (isTrue(tc
281                                 .getProperty(PropertyRegistry.TABLE_USE_COMPOUND_PROPERTY_NAMES))) {
282                     sb.setLength(0);
283                     sb.append(calculatedColumnName);
284                     sb.append('_');
285                     sb.append(getCamelCaseString(
286                             introspectedColumn.getRemarks(), true));
287                     introspectedColumn.setJavaProperty(
288                             getValidPropertyName(sb.toString()));
289                 } else {
290                     introspectedColumn.setJavaProperty(
291                             getCamelCaseString(calculatedColumnName, false));
292                 }
293 
294                 FullyQualifiedJavaType fullyQualifiedJavaType = javaTypeResolver
295                         .calculateJavaType(introspectedColumn);
296 
297                 if (fullyQualifiedJavaType != null) {
298                     introspectedColumn
299                             .setFullyQualifiedJavaType(fullyQualifiedJavaType);
300                     introspectedColumn.setJdbcTypeName(javaTypeResolver
301                             .calculateJdbcTypeName(introspectedColumn));
302                 } else {
303                     // type cannot be resolved. Check for ignored or overridden
304                     boolean warn = true;
305                     if (tc.isColumnIgnored(introspectedColumn
306                             .getActualColumnName())) {
307                         warn = false;
308                     }
309 
310                     ColumnOverride co = tc.getColumnOverride(introspectedColumn
311                             .getActualColumnName());
312                     if (co != null) {
313                         if (stringHasValue(co.getJavaType())
314                                 && stringHasValue(co.getJavaType())) {
315                             warn = false;
316                         }
317                     }
318 
319                     // if the type is not supported, then we'll report a warning
320                     if (warn) {
321                         introspectedColumn
322                                 .setFullyQualifiedJavaType(FullyQualifiedJavaType
323                                         .getObjectInstance());
324                         introspectedColumn.setJdbcTypeName("OTHER"); //$NON-NLS-1$
325 
326                         String warning = getString("Warning.14", //$NON-NLS-1$
327                                 Integer.toString(introspectedColumn.getJdbcType()),
328                                 entry.getKey().toString(),
329                                 introspectedColumn.getActualColumnName());
330 
331                         warnings.add(warning);
332                     }
333                 }
334 
335                 if (context.autoDelimitKeywords()) {
336                     if (SqlReservedWords.containsWord(introspectedColumn
337                             .getActualColumnName())) {
338                         introspectedColumn.setColumnNameDelimited(true);
339                     }
340                 }
341 
342                 if (tc.isAllColumnDelimitingEnabled()) {
343                     introspectedColumn.setColumnNameDelimited(true);
344                 }
345             }
346         }
347     }
348 
349     private void calculateIdentityColumns(TableConfiguration tc,
350             Map<ActualTableName, List<IntrospectedColumn>> columns) {
351         GeneratedKey gk = tc.getGeneratedKey();
352         if (gk == null) {
353             // no generated key, then no identity or sequence columns
354             return;
355         }
356         
357         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
358                 .entrySet()) {
359             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
360                 if (isMatchedColumn(introspectedColumn, gk)) {
361                     if (gk.isIdentity() || gk.isJdbcStandard()) {
362                         introspectedColumn.setIdentity(true);
363                         introspectedColumn.setSequenceColumn(false);
364                     } else {
365                         introspectedColumn.setIdentity(false);
366                         introspectedColumn.setSequenceColumn(true);
367                     }
368                 }
369             }
370         }
371     }
372     
373     private boolean isMatchedColumn(IntrospectedColumn introspectedColumn, GeneratedKey gk) {
374         if (introspectedColumn.isColumnNameDelimited()) {
375             return introspectedColumn.getActualColumnName().equals(gk.getColumn());
376         } else {
377             return introspectedColumn.getActualColumnName().equalsIgnoreCase(gk.getColumn());
378         }
379     }
380 
381     private void applyColumnOverrides(TableConfiguration tc,
382             Map<ActualTableName, List<IntrospectedColumn>> columns) {
383         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
384                 .entrySet()) {
385             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
386                 ColumnOverride columnOverride = tc
387                         .getColumnOverride(introspectedColumn
388                                 .getActualColumnName());
389 
390                 if (columnOverride != null) {
391                     if (logger.isDebugEnabled()) {
392                         logger.debug(getString("Tracing.4", //$NON-NLS-1$
393                                 introspectedColumn.getActualColumnName(), entry
394                                         .getKey().toString()));
395                     }
396 
397                     if (stringHasValue(columnOverride
398                             .getJavaProperty())) {
399                         introspectedColumn.setJavaProperty(columnOverride
400                                 .getJavaProperty());
401                     }
402 
403                     if (stringHasValue(columnOverride
404                             .getJavaType())) {
405                         introspectedColumn
406                                 .setFullyQualifiedJavaType(new FullyQualifiedJavaType(
407                                         columnOverride.getJavaType()));
408                     }
409 
410                     if (stringHasValue(columnOverride
411                             .getJdbcType())) {
412                         introspectedColumn.setJdbcTypeName(columnOverride
413                                 .getJdbcType());
414                     }
415 
416                     if (stringHasValue(columnOverride
417                             .getTypeHandler())) {
418                         introspectedColumn.setTypeHandler(columnOverride
419                                 .getTypeHandler());
420                     }
421 
422                     if (columnOverride.isColumnNameDelimited()) {
423                         introspectedColumn.setColumnNameDelimited(true);
424                     }
425 
426                     introspectedColumn.setProperties(columnOverride
427                             .getProperties());
428                 }
429             }
430         }
431     }
432 
433     /**
434      * This method returns a Map<ActualTableName, List<ColumnDefinitions>> of
435      * columns returned from the database introspection.
436      * 
437      * @param tc
438      * @return introspected columns
439      * @throws SQLException
440      */
441     private Map<ActualTableName, List<IntrospectedColumn>> getColumns(
442             TableConfiguration tc) throws SQLException {
443         String localCatalog;
444         String localSchema;
445         String localTableName;
446 
447         boolean delimitIdentifiers = tc.isDelimitIdentifiers()
448                 || stringContainsSpace(tc.getCatalog())
449                 || stringContainsSpace(tc.getSchema())
450                 || stringContainsSpace(tc.getTableName());
451 
452         if (delimitIdentifiers) {
453             localCatalog = tc.getCatalog();
454             localSchema = tc.getSchema();
455             localTableName = tc.getTableName();
456         } else if (databaseMetaData.storesLowerCaseIdentifiers()) {
457             localCatalog = tc.getCatalog() == null ? null : tc.getCatalog()
458                     .toLowerCase();
459             localSchema = tc.getSchema() == null ? null : tc.getSchema()
460                     .toLowerCase();
461             localTableName = tc.getTableName() == null ? null : tc
462                     .getTableName().toLowerCase();
463         } else if (databaseMetaData.storesUpperCaseIdentifiers()) {
464             localCatalog = tc.getCatalog() == null ? null : tc.getCatalog()
465                     .toUpperCase();
466             localSchema = tc.getSchema() == null ? null : tc.getSchema()
467                     .toUpperCase();
468             localTableName = tc.getTableName() == null ? null : tc
469                     .getTableName().toUpperCase();
470         } else {
471             localCatalog = tc.getCatalog();
472             localSchema = tc.getSchema();
473             localTableName = tc.getTableName();
474         }
475 
476         if (tc.isWildcardEscapingEnabled()) {
477             String escapeString = databaseMetaData.getSearchStringEscape();
478 
479             StringBuilder sb = new StringBuilder();
480             StringTokenizer st;
481             if (localSchema != null) {
482                 st = new StringTokenizer(localSchema, "_%", true); //$NON-NLS-1$
483                 while (st.hasMoreTokens()) {
484                     String token = st.nextToken();
485                     if (token.equals("_") //$NON-NLS-1$
486                             || token.equals("%")) { //$NON-NLS-1$
487                         sb.append(escapeString);
488                     }
489                     sb.append(token);
490                 }
491                 localSchema = sb.toString();
492             }
493 
494             sb.setLength(0);
495             st = new StringTokenizer(localTableName, "_%", true); //$NON-NLS-1$
496             while (st.hasMoreTokens()) {
497                 String token = st.nextToken();
498                 if (token.equals("_") //$NON-NLS-1$
499                         || token.equals("%")) { //$NON-NLS-1$
500                     sb.append(escapeString);
501                 }
502                 sb.append(token);
503             }
504             localTableName = sb.toString();
505         }
506 
507         Map<ActualTableName, List<IntrospectedColumn>> answer = new HashMap<ActualTableName, List<IntrospectedColumn>>();
508 
509         if (logger.isDebugEnabled()) {
510             String fullTableName = composeFullyQualifiedTableName(localCatalog, localSchema,
511                             localTableName, '.');
512             logger.debug(getString("Tracing.1", fullTableName)); //$NON-NLS-1$
513         }
514 
515         ResultSet rs = databaseMetaData.getColumns(localCatalog, localSchema,
516                 localTableName, null);
517 
518         while (rs.next()) {
519             IntrospectedColumn introspectedColumn = ObjectFactory
520                     .createIntrospectedColumn(context);
521 
522             introspectedColumn.setTableAlias(tc.getAlias());
523             introspectedColumn.setJdbcType(rs.getInt("DATA_TYPE")); //$NON-NLS-1$
524             introspectedColumn.setLength(rs.getInt("COLUMN_SIZE")); //$NON-NLS-1$
525             introspectedColumn.setActualColumnName(rs.getString("COLUMN_NAME")); //$NON-NLS-1$
526             introspectedColumn
527                     .setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable); //$NON-NLS-1$
528             introspectedColumn.setScale(rs.getInt("DECIMAL_DIGITS")); //$NON-NLS-1$
529             introspectedColumn.setRemarks(rs.getString("REMARKS")); //$NON-NLS-1$
530             introspectedColumn.setDefaultValue(rs.getString("COLUMN_DEF")); //$NON-NLS-1$
531 
532             ActualTableName atn = new ActualTableName(
533                     rs.getString("TABLE_CAT"), //$NON-NLS-1$
534                     rs.getString("TABLE_SCHEM"), //$NON-NLS-1$
535                     rs.getString("TABLE_NAME")); //$NON-NLS-1$
536 
537             List<IntrospectedColumn> columns = answer.get(atn);
538             if (columns == null) {
539                 columns = new ArrayList<IntrospectedColumn>();
540                 answer.put(atn, columns);
541             }
542 
543             columns.add(introspectedColumn);
544 
545             if (logger.isDebugEnabled()) {
546                 logger.debug(getString(
547                         "Tracing.2", //$NON-NLS-1$
548                         introspectedColumn.getActualColumnName(), Integer
549                                 .toString(introspectedColumn.getJdbcType()),
550                         atn.toString()));
551             }
552         }
553 
554         closeResultSet(rs);
555 
556         if (answer.size() > 1
557                 && !stringContainsSQLWildcard(localSchema)
558                 && !stringContainsSQLWildcard(localTableName)) {
559             // issue a warning if there is more than one table and
560             // no wildcards were used
561             ActualTableName inputAtn = new ActualTableName(tc.getCatalog(), tc
562                     .getSchema(), tc.getTableName());
563 
564             StringBuilder sb = new StringBuilder();
565             boolean comma = false;
566             for (ActualTableName atn : answer.keySet()) {
567                 if (comma) {
568                     sb.append(',');
569                 } else {
570                     comma = true;
571                 }
572                 sb.append(atn.toString());
573             }
574 
575             warnings.add(getString("Warning.25", //$NON-NLS-1$
576                     inputAtn.toString(), sb.toString()));
577         }
578 
579         return answer;
580     }
581 
582     private List<IntrospectedTable> calculateIntrospectedTables(
583             TableConfiguration tc,
584             Map<ActualTableName, List<IntrospectedColumn>> columns) {
585         boolean delimitIdentifiers = tc.isDelimitIdentifiers()
586                 || stringContainsSpace(tc.getCatalog())
587                 || stringContainsSpace(tc.getSchema())
588                 || stringContainsSpace(tc.getTableName());
589 
590         List<IntrospectedTable> answer = new ArrayList<IntrospectedTable>();
591 
592         for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns
593                 .entrySet()) {
594             ActualTableName atn = entry.getKey();
595 
596             // we only use the returned catalog and schema if something was
597             // actually
598             // specified on the table configuration. If something was returned
599             // from the DB for these fields, but nothing was specified on the
600             // table
601             // configuration, then some sort of DB default is being returned
602             // and we don't want that in our SQL
603             FullyQualifiedTable table = new FullyQualifiedTable(
604                     stringHasValue(tc.getCatalog()) ? atn
605                             .getCatalog() : null,
606                     stringHasValue(tc.getSchema()) ? atn
607                             .getSchema() : null,
608                     atn.getTableName(),
609                     tc.getDomainObjectName(),
610                     tc.getAlias(),
611                     isTrue(tc.getProperty(PropertyRegistry.TABLE_IGNORE_QUALIFIERS_AT_RUNTIME)),
612                     tc.getProperty(PropertyRegistry.TABLE_RUNTIME_CATALOG),
613                     tc.getProperty(PropertyRegistry.TABLE_RUNTIME_SCHEMA),
614                     tc.getProperty(PropertyRegistry.TABLE_RUNTIME_TABLE_NAME),
615                     delimitIdentifiers, context);
616 
617             IntrospectedTable introspectedTable = ObjectFactory
618                     .createIntrospectedTable(tc, table, context);
619 
620             for (IntrospectedColumn introspectedColumn : entry.getValue()) {
621                 introspectedTable.addColumn(introspectedColumn);
622             }
623 
624             calculatePrimaryKey(table, introspectedTable);
625 
626             answer.add(introspectedTable);
627         }
628 
629         return answer;
630     }
631 }