View Javadoc
1   /*
2    * This file is a part of the SchemaSpy project (http://schemaspy.sourceforge.net).
3    * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 John Currier
4    *
5    * SchemaSpy is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *
10   * SchemaSpy is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package net.sourceforge.schemaspy.model;
20  
21  import java.sql.DatabaseMetaData;
22  import java.sql.PreparedStatement;
23  import java.sql.ResultSet;
24  import java.sql.ResultSetMetaData;
25  import java.sql.SQLException;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.Comparator;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.TreeMap;
36  import java.util.TreeSet;
37  import java.util.logging.Logger;
38  import java.util.regex.Pattern;
39  import net.sourceforge.schemaspy.Config;
40  import net.sourceforge.schemaspy.model.xml.ForeignKeyMeta;
41  import net.sourceforge.schemaspy.model.xml.TableColumnMeta;
42  import net.sourceforge.schemaspy.model.xml.TableMeta;
43  import net.sourceforge.schemaspy.util.CaseInsensitiveMap;
44  
45  /**
46   * A <code>Table</code> is one of the basic building blocks of SchemaSpy
47   * that knows everything about the database table's metadata.
48   *
49   * @author John Currier
50   */
51  public class Table implements Comparable<Table> {
52      private final String schema;
53      private final String name;
54      protected final CaseInsensitiveMap<TableColumn> columns = new CaseInsensitiveMap<TableColumn>();
55      private final List<TableColumn> primaryKeys = new ArrayList<TableColumn>();
56      private final CaseInsensitiveMap<ForeignKeyConstraint> foreignKeys = new CaseInsensitiveMap<ForeignKeyConstraint>();
57      private final CaseInsensitiveMap<TableIndex> indexes = new CaseInsensitiveMap<TableIndex>();
58      private       Object id;
59      private final Map<String, String> checkConstraints = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
60      private Long numRows;
61      protected final Database db;
62      protected final Properties properties;
63      private       String comments;
64      private int maxChildren;
65      private int maxParents;
66      private final static Logger logger = Logger.getLogger(Table.class.getName());
67  
68      /**
69       * Construct a table that knows everything about the database table's metadata
70       *
71       * @param db
72       * @param schema
73       * @param name
74       * @param comments
75       * @param properties
76       * @param excludeIndirectColumns
77       * @param excludeColumns
78       * @throws SQLException
79       */
80      public Table(Database db, String schema, String name, String comments, Properties properties, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
81          this.schema = schema;
82          this.name = name;
83          this.db = db;
84          this.properties = properties;
85          logger.fine("Creating " + getClass().getSimpleName().toLowerCase() + " " +
86                  schema == null ? name : (schema + '.' + name));
87          setComments(comments);
88          initColumns(excludeIndirectColumns, excludeColumns);
89          initIndexes();
90          initPrimaryKeys(db.getMetaData());
91      }
92  
93      /**
94       * "Connect" all of this table's foreign keys to their referenced primary keys
95       * (and, in some cases, do the reverse as well).
96       *
97       * @param tables
98       * @param excludeIndirectColumns
99       * @param excludeColumns
100      * @throws SQLException
101      */
102     public void connectForeignKeys(Map<String, Table> tables, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
103         ResultSet rs = null;
104 
105         try {
106             rs = db.getMetaData().getImportedKeys(null, getSchema(), getName());
107 
108             while (rs.next()) {
109                 addForeignKey(rs.getString("FK_NAME"), rs.getString("FKCOLUMN_NAME"),
110                         rs.getString("PKTABLE_SCHEM"), rs.getString("PKTABLE_NAME"),
111                         rs.getString("PKCOLUMN_NAME"),
112                         rs.getInt("UPDATE_RULE"), rs.getInt("DELETE_RULE"),
113                         tables, excludeIndirectColumns, excludeColumns);
114             }
115         } finally {
116             if (rs != null)
117                 rs.close();
118         }
119 
120         // also try to find all of the 'remote' tables in other schemas that
121         // point to our primary keys (not necessary in the normal case
122         // as we infer this from the opposite direction)
123         if (getSchema() != null) {
124             try {
125                 rs = db.getMetaData().getExportedKeys(null, getSchema(), getName());
126 
127                 while (rs.next()) {
128                     String otherSchema = rs.getString("FKTABLE_SCHEM");
129                     if (!getSchema().equals(otherSchema))
130                         db.addRemoteTable(otherSchema, rs.getString("FKTABLE_NAME"), getSchema(), properties, excludeIndirectColumns, excludeColumns);
131                 }
132             } finally {
133                 if (rs != null)
134                     rs.close();
135             }
136         }
137     }
138 
139     /**
140      * Get the foreign keys associated with this table
141      *
142      * @return
143      */
144     public Collection<ForeignKeyConstraint> getForeignKeys() {
145         return Collections.unmodifiableCollection(foreignKeys.values());
146     }
147 
148     /**
149      * Add a check constraint to the table
150      * (no real details, just name and textual representation)
151      *
152      * @param constraintName
153      * @param text
154      */
155     public void addCheckConstraint(String constraintName, String text) {
156         checkConstraints.put(constraintName, text);
157     }
158 
159     /**
160      * @throws SQLException
161      */
162     protected void addForeignKey(String fkName, String fkColName,
163                         String pkTableSchema, String pkTableName, String pkColName,
164                         int updateRule, int deleteRule,
165                         Map<String, Table> tables,
166                         Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
167         if (fkName == null)
168             return;
169 
170         ForeignKeyConstraint foreignKey = foreignKeys.get(fkName);
171 
172         if (foreignKey == null) {
173             foreignKey = new ForeignKeyConstraint(this, fkName, updateRule, deleteRule);
174 
175             foreignKeys.put(fkName, foreignKey);
176         }
177 
178         TableColumn childColumn = getColumn(fkColName);
179         if (childColumn != null) {
180             foreignKey.addChildColumn(childColumn);
181 
182             Table parentTable = tables.get(pkTableName);
183             String parentSchema = pkTableSchema;
184             String baseSchema = Config.getInstance().getSchema();
185 
186             // if named table doesn't exist in this schema
187             // or exists here but really referencing same named table in another schema
188             if (parentTable == null ||
189                     (baseSchema != null && parentSchema != null &&
190                      !baseSchema.equals(parentSchema))) {
191                 parentTable = db.addRemoteTable(parentSchema, pkTableName, baseSchema,
192                                         properties, excludeIndirectColumns, excludeColumns);
193             }
194 
195             if (parentTable != null) {
196                 TableColumn parentColumn = parentTable.getColumn(pkColName);
197                 if (parentColumn != null) {
198                     foreignKey.addParentColumn(parentColumn);
199 
200                     childColumn.addParent(parentColumn, foreignKey);
201                     parentColumn.addChild(childColumn, foreignKey);
202                 } else {
203                     logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
204                                         "' - Column '" + pkColName + "' doesn't exist in table '" + parentTable + "'");
205                 }
206             } else {
207                 logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
208                                     "' - Unknown Referenced Table '" + pkTableName + "'");
209             }
210         } else {
211             logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
212                                 "' - Column '" + fkColName + "' doesn't exist");
213         }
214     }
215 
216     /**
217      * @param meta
218      * @throws SQLException
219      */
220     private void initPrimaryKeys(DatabaseMetaData meta) throws SQLException {
221         if (properties == null)
222             return;
223 
224         ResultSet rs = null;
225 
226         try {
227             rs = meta.getPrimaryKeys(null, getSchema(), getName());
228 
229             while (rs.next())
230                 setPrimaryColumn(rs);
231         } finally {
232             if (rs != null)
233                 rs.close();
234         }
235     }
236 
237     /**
238      * @param rs
239      * @throws SQLException
240      */
241     private void setPrimaryColumn(ResultSet rs) throws SQLException {
242         String pkName = rs.getString("PK_NAME");
243         if (pkName == null)
244             return;
245 
246         TableIndex index = getIndex(pkName);
247         if (index != null) {
248             index.setIsPrimaryKey(true);
249         }
250 
251         String columnName = rs.getString("COLUMN_NAME");
252 
253         setPrimaryColumn(getColumn(columnName));
254     }
255 
256     /**
257      * @param primaryColumn
258      */
259     void setPrimaryColumn(TableColumn primaryColumn) {
260         primaryKeys.add(primaryColumn);
261     }
262 
263     /**
264      * @param excludeIndirectColumns
265      * @param excludeColumns
266      * @throws SQLException
267      */
268     private void initColumns(Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
269         ResultSet rs = null;
270 
271         synchronized (Table.class) {
272             try {
273                 rs = db.getMetaData().getColumns(null, getSchema(), getName(), "%");
274 
275                 while (rs.next())
276                     addColumn(rs, excludeIndirectColumns, excludeColumns);
277             } catch (SQLException exc) {
278                 class ColumnInitializationFailure extends SQLException {
279                     private static final long serialVersionUID = 1L;
280 
281                     public ColumnInitializationFailure(SQLException failure) {
282                         super("Failed to collect column details for " + (isView() ? "view" : "table") + " '" + getName() + "' in schema '" + getSchema() + "'");
283                         initCause(failure);
284                     }
285                 }
286 
287                 throw new ColumnInitializationFailure(exc);
288             } finally {
289                 if (rs != null)
290                     rs.close();
291             }
292         }
293 
294         if (!isView() && !isRemote())
295             initColumnAutoUpdate(false);
296     }
297 
298     /**
299      * @param forceQuotes
300      * @throws SQLException
301      */
302     private void initColumnAutoUpdate(boolean forceQuotes) throws SQLException {
303         ResultSet rs = null;
304         PreparedStatement stmt = null;
305 
306         // we've got to get a result set with all the columns in it
307         // so we can ask if the columns are auto updated
308         // Ugh!!!  Should have been in DatabaseMetaData instead!!!
309         StringBuilder sql = new StringBuilder("select * from ");
310         if (getSchema() != null) {
311             sql.append(getSchema());
312             sql.append('.');
313         }
314 
315         if (forceQuotes) {
316             String quote = db.getMetaData().getIdentifierQuoteString().trim();
317             sql.append(quote + getName() + quote);
318         } else
319             sql.append(db.getQuotedIdentifier(getName()));
320 
321         sql.append(" where 0 = 1");
322 
323         try {
324             stmt = db.getMetaData().getConnection().prepareStatement(sql.toString());
325             rs = stmt.executeQuery();
326 
327             ResultSetMetaData rsMeta = rs.getMetaData();
328             for (int i = rsMeta.getColumnCount(); i > 0; --i) {
329                 TableColumn column = getColumn(rsMeta.getColumnName(i));
330                 column.setIsAutoUpdated(rsMeta.isAutoIncrement(i));
331             }
332         } catch (SQLException exc) {
333             if (forceQuotes) {
334                 // don't completely choke just because we couldn't do this....
335                 logger.warning("Failed to determine auto increment status: " + exc);
336                 logger.warning("SQL: " + sql.toString());
337             } else {
338                 initColumnAutoUpdate(true);
339             }
340         } finally {
341             if (rs != null)
342                 rs.close();
343             if (stmt != null)
344                 stmt.close();
345         }
346     }
347 
348     /**
349      * @param rs - from {@link DatabaseMetaData#getColumns(String, String, String, String)}
350      * @param excludeIndirectColumns
351      * @param excludeColumns
352      * @throws SQLException
353      */
354     protected void addColumn(ResultSet rs, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
355         String columnName = rs.getString("COLUMN_NAME");
356 
357         if (columnName == null)
358             return;
359 
360         if (getColumn(columnName) == null) {
361             TableColumn column = new TableColumn(this, rs, excludeIndirectColumns, excludeColumns);
362 
363             columns.put(column.getName(), column);
364         }
365     }
366 
367     /**
368      * Add a column that's defined in xml metadata.
369      * Assumes that a column named colMeta.getName() doesn't already exist in <code>columns</code>.
370      * @param colMeta
371      * @return
372      */
373     protected TableColumn addColumn(TableColumnMeta colMeta) {
374         TableColumn column = new TableColumn(this, colMeta);
375 
376         columns.put(column.getName(), column);
377 
378         return column;
379     }
380 
381     /**
382      * Initialize index information
383      *
384      * @throws SQLException
385      */
386     private void initIndexes() throws SQLException {
387         if (isView() || isRemote())
388             return;
389 
390         // first try to initialize using the index query spec'd in the .properties
391         // do this first because some DB's (e.g. Oracle) do 'bad' things with getIndexInfo()
392         // (they try to do a DDL analyze command that has some bad side-effects)
393         if (initIndexes(properties.getProperty("selectIndexesSql")))
394             return;
395 
396         // couldn't, so try the old fashioned approach
397         ResultSet rs = null;
398 
399         try {
400             rs = db.getMetaData().getIndexInfo(null, getSchema(), getName(), false, true);
401 
402             while (rs.next()) {
403                 if (rs.getShort("TYPE") != DatabaseMetaData.tableIndexStatistic)
404                     addIndex(rs);
405             }
406         } catch (SQLException exc) {
407             logger.warning("Unable to extract index info for table '" + getName() + "' in schema '" + getSchema() + "': " + exc);
408         } finally {
409             if (rs != null)
410                 rs.close();
411         }
412     }
413 
414     /**
415      * Try to initialize index information based on the specified SQL
416      *
417      * @return boolean <code>true</code> if it worked, otherwise <code>false</code>
418      */
419     private boolean initIndexes(String selectIndexesSql) {
420         if (selectIndexesSql == null)
421             return false;
422 
423         PreparedStatement stmt = null;
424         ResultSet rs = null;
425 
426         try {
427             stmt = db.prepareStatement(selectIndexesSql, getName());
428             rs = stmt.executeQuery();
429 
430             while (rs.next()) {
431                 if (rs.getShort("TYPE") != DatabaseMetaData.tableIndexStatistic)
432                     addIndex(rs);
433             }
434         } catch (SQLException sqlException) {
435             logger.warning("Failed to query index information with SQL: " + selectIndexesSql);
436             logger.warning(sqlException.toString());
437             return false;
438         } finally {
439             if (rs != null) {
440                 try {
441                     rs.close();
442                 } catch (Exception exc) {
443                     exc.printStackTrace();
444                 }
445             }
446             if (stmt != null)  {
447                 try {
448                     stmt.close();
449                 } catch (Exception exc) {
450                     exc.printStackTrace();
451                 }
452             }
453         }
454 
455         return true;
456     }
457 
458     /**
459      * @param indexName
460      * @return
461      */
462     public TableIndex getIndex(String indexName) {
463         return indexes.get(indexName);
464     }
465 
466     /**
467      * @param rs
468      * @throws SQLException
469      */
470     private void addIndex(ResultSet rs) throws SQLException {
471         String indexName = rs.getString("INDEX_NAME");
472 
473         if (indexName == null)
474             return;
475 
476         TableIndex index = getIndex(indexName);
477 
478         if (index == null) {
479             index = new TableIndex(rs);
480 
481             indexes.put(index.getName(), index);
482         }
483 
484         index.addColumn(getColumn(rs.getString("COLUMN_NAME")), rs.getString("ASC_OR_DESC"));
485     }
486 
487     /**
488      * Returns the schema that the table belongs to
489      *
490      * @return
491      */
492     public String getSchema() {
493         return schema;
494     }
495 
496     /**
497      * Returns the name of the table
498      *
499      * @return
500      */
501     public String getName() {
502         return name;
503     }
504 
505     /**
506      * Object IDs are useful for tables such as DB/2 that many times
507      * give error messages based on object ID and not name
508      *
509      * @param id
510      */
511     public void setId(Object id) {
512         this.id = id;
513     }
514 
515     /**
516      * @see #setId(Object)
517      *
518      * @return
519      */
520     public Object getId() {
521         return id;
522     }
523 
524     /**
525      * Returns the check constraints associated with this table
526      *
527      * @return
528      */
529     public Map<String, String> getCheckConstraints() {
530         return checkConstraints;
531     }
532 
533     /**
534      * Returns the indexes that are applied to this table
535      *
536      * @return
537      */
538     public Set<TableIndex> getIndexes() {
539         return new HashSet<TableIndex>(indexes.values());
540     }
541 
542     /**
543      * Returns a collection of table columns that have been identified as "primary"
544      *
545      * @return
546      */
547     public List<TableColumn> getPrimaryColumns() {
548         return primaryKeys;
549     }
550 
551     /**
552      * @return Comments associated with this table, or <code>null</code> if none.
553      */
554     public String getComments() {
555         return comments;
556     }
557 
558     /**
559      * Sets the comments that are associated with this table
560      *
561      * @param comments
562      */
563     public void setComments(String comments) {
564         String cmts = (comments == null || comments.trim().length() == 0) ? null : comments.trim();
565 
566         // MySQL's InnoDB engine does some insane crap of storing erroneous details in
567         // with table comments.  Here I attempt to strip the "crap" out without impacting
568         // other databases.  Ideally this should happen in selectColumnCommentsSql (and
569         // therefore isolate it to MySQL), but it's a bit too complex to do cleanly.
570         if (cmts != null) {
571             int crapIndex = cmts.indexOf("; InnoDB free: ");
572             if (crapIndex == -1)
573                 crapIndex = cmts.startsWith("InnoDB free: ") ? 0 : -1;
574             if (crapIndex != -1) {
575                 cmts = cmts.substring(0, crapIndex).trim();
576                 cmts = cmts.length() == 0 ? null : cmts;
577             }
578         }
579 
580         this.comments = cmts;
581     }
582 
583     /**
584      * Returns the {@link TableColumn} with the given name, or <code>null</code>
585      * if it doesn't exist
586      *
587      * @param columnName
588      * @return
589      */
590     public TableColumn getColumn(String columnName) {
591         return columns.get(columnName);
592     }
593 
594     /**
595      * Returns <code>List</code> of <code>TableColumn</code>s in ascending column number order.
596      *
597      * @return
598      */
599     public List<TableColumn> getColumns() {
600         Set<TableColumn> sorted = new TreeSet<TableColumn>(new ByColumnIdComparator());
601         sorted.addAll(columns.values());
602         return new ArrayList<TableColumn>(sorted);
603     }
604 
605     /**
606      * Returns <code>true</code> if this table references no other tables..<p/>
607      * Used in dependency analysis.
608      * @return
609      */
610     public boolean isRoot() {
611         for (TableColumn column : columns.values()) {
612             if (column.isForeignKey()) {
613                 return false;
614             }
615         }
616 
617         return true;
618     }
619 
620     /**
621      * Returns <code>true</code> if this table is referenced by no other tables.<p/>
622      * Used in dependency analysis.
623      * @return
624      */
625     public boolean isLeaf() {
626         for (TableColumn column : columns.values()) {
627             if (!column.getChildren().isEmpty()) {
628                 return false;
629             }
630         }
631 
632         return true;
633     }
634 
635     /**
636      * Returns the maximum number of parents that this table has had before
637      * any had been removed during dependency analysis
638      *
639      * @return
640      */
641     public int getMaxParents() {
642         return maxParents;
643     }
644 
645     /**
646      * Notification that's called to indicate that a parent has been added to
647      * this table
648      */
649     public void addedParent() {
650         maxParents++;
651     }
652 
653     /**
654      * "Unlink" all of the parent tables from this table
655      */
656     public void unlinkParents() {
657         for (TableColumn column : columns.values()) {
658             column.unlinkParents();
659         }
660     }
661 
662     /**
663      * Returns the maximum number of children that this table has had before
664      * any had been removed during dependency analysis
665      *
666      * @return
667      */
668     public int getMaxChildren() {
669         return maxChildren;
670     }
671 
672     /**
673      * Notification that's called to indicate that a child has been added to
674      * this table
675      */
676     public void addedChild() {
677         maxChildren++;
678     }
679 
680     /**
681      * "Unlink" all of the child tables from this table
682      */
683     public void unlinkChildren() {
684         for (TableColumn column : columns.values()) {
685             column.unlinkChildren();
686         }
687     }
688 
689     /**
690      * Remove a single self referencing constraint if one exists.
691      *
692      * @return
693      */
694     public ForeignKeyConstraint removeSelfReferencingConstraint() {
695         return remove(getSelfReferencingConstraint());
696     }
697 
698     /**
699      * Remove the specified {@link ForeignKeyConstraint} from this table.<p>
700      *
701      * This is a more drastic removal solution that was proposed by Remke Rutgers
702      *
703      * @param constraint
704      */
705     private ForeignKeyConstraint remove(ForeignKeyConstraint constraint) {
706         if (constraint != null) {
707             for (int i = 0; i < constraint.getChildColumns().size(); i++) {
708                 TableColumn childColumn = constraint.getChildColumns().get(i);
709                 TableColumn parentColumn = constraint.getParentColumns().get(i);
710                 childColumn.removeParent(parentColumn);
711                 parentColumn.removeChild(childColumn);
712             }
713         }
714         return constraint;
715     }
716 
717     /**
718      * Return a self referencing constraint if one exists
719      *
720      * @return
721      */
722     private ForeignKeyConstraint getSelfReferencingConstraint() {
723         for (TableColumn column : columns.values()) {
724             for (TableColumn parentColumn : column.getParents()) {
725                 if (compareTo(parentColumn.getTable()) == 0) {
726                     return column.getParentConstraint(parentColumn);
727                 }
728             }
729         }
730         return null;
731     }
732 
733     /**
734      * Remove any non-real foreign keys
735      *
736      * @return
737      */
738     public List<ForeignKeyConstraint> removeNonRealForeignKeys() {
739         List<ForeignKeyConstraint> nonReals = new ArrayList<ForeignKeyConstraint>();
740 
741         for (TableColumn column : columns.values()) {
742             for (TableColumn parentColumn : column.getParents()) {
743                 ForeignKeyConstraint constraint = column.getParentConstraint(parentColumn);
744                 if (constraint != null && !constraint.isReal()) {
745                     nonReals.add(constraint);
746                 }
747             }
748         }
749 
750         // remove constraints outside of above loop to prevent
751         // concurrent modification exceptions while iterating
752         for (ForeignKeyConstraint constraint : nonReals) {
753             remove(constraint);
754         }
755 
756         return nonReals;
757     }
758 
759     /**
760      * Returns the number of tables that reference this table
761      *
762      * @return
763      */
764     public int getNumChildren() {
765         int numChildren = 0;
766 
767         for (TableColumn column : columns.values()) {
768             numChildren += column.getChildren().size();
769         }
770 
771         return numChildren;
772     }
773 
774     /**
775      * Returns the number of non-implied children
776      * @return
777      */
778     public int getNumNonImpliedChildren() {
779         int numChildren = 0;
780 
781         for (TableColumn column : columns.values()) {
782             for (TableColumn childColumn : column.getChildren()) {
783                 if (!column.getChildConstraint(childColumn).isImplied())
784                     ++numChildren;
785             }
786         }
787 
788         return numChildren;
789     }
790 
791     /**
792      * Returns the number of tables that are referenced by this table
793      *
794      * @return
795      */
796     public int getNumParents() {
797         int numParents = 0;
798 
799         for (TableColumn column : columns.values()) {
800             numParents += column.getParents().size();
801         }
802 
803         return numParents;
804     }
805 
806     /**
807      * Returns the number of non-implied parents
808      *
809      * @return
810      */
811     public int getNumNonImpliedParents() {
812         int numParents = 0;
813 
814         for (TableColumn column : columns.values()) {
815             for (TableColumn parentColumn : column.getParents()) {
816                 if (!column.getParentConstraint(parentColumn).isImplied())
817                     ++numParents;
818             }
819         }
820 
821         return numParents;
822     }
823 
824     /**
825      * Remove one foreign key constraint.
826      *
827      * <p/>Used during dependency analysis phase.
828      *
829      * @return
830      */
831     public ForeignKeyConstraint removeAForeignKeyConstraint() {
832         @SuppressWarnings("hiding")
833         final List<TableColumn> columns = getColumns();
834         int numParents = 0;
835         int numChildren = 0;
836         // remove either a child or parent, choosing which based on which has the
837         // least number of foreign key associations (when either gets to zero then
838         // the table can be pruned)
839         for (TableColumn column : columns) {
840             numParents += column.getParents().size();
841             numChildren += column.getChildren().size();
842         }
843 
844         for (TableColumn column : columns) {
845             ForeignKeyConstraint constraint;
846             if (numParents <= numChildren)
847                 constraint = column.removeAParentFKConstraint();
848             else
849                 constraint = column.removeAChildFKConstraint();
850             if (constraint != null)
851                 return constraint;
852         }
853 
854         return null;
855     }
856 
857     /**
858      * Returns <code>true</code> if this is a view, <code>false</code> otherwise
859      *
860      * @return
861      */
862     public boolean isView() {
863         return false;
864     }
865 
866     /**
867      * Returns <code>true</code> if this table is remote (in another schema), <code>false</code> otherwise
868      * @return
869      */
870     public boolean isRemote() {
871         return false;
872     }
873 
874     /**
875      * If this is a view it returns the SQL used to create the view (if it's available).
876      * <code>null</code> if it's not a view or the SQL isn't available.
877      * @return
878      * @see #isView()
879      */
880     public String getViewSql() {
881         return null;
882     }
883 
884     /**
885      * Returns the number of rows contained in this table, or -1 if unable to determine
886      * the number of rows.
887      *
888      * @return
889      */
890     public long getNumRows() {
891         if (numRows == null) {
892             numRows = Config.getInstance().isNumRowsEnabled() ? fetchNumRows() : -1;
893         }
894 
895         return numRows;
896     }
897 
898     /**
899      * Explicitly set the number of rows in this table
900      *
901      * @param numRows
902      */
903     public void setNumRows(long numRows) {
904         this.numRows = numRows;
905     }
906 
907     /**
908      * Fetch the number of rows contained in this table.
909      *
910      * returns -1 if unable to successfully fetch the row count
911      *
912      * @return long
913      * @throws SQLException
914      */
915     protected long fetchNumRows() {
916         if (properties == null) // some "meta" tables don't have associated properties
917             return 0;
918 
919         SQLException originalFailure = null;
920 
921         String sql = properties.getProperty("selectRowCountSql");
922         if (sql != null) {
923             PreparedStatement stmt = null;
924             ResultSet rs = null;
925 
926             try {
927                 stmt = db.prepareStatement(sql, getName());
928                 rs = stmt.executeQuery();
929 
930                 while (rs.next()) {
931                     return rs.getLong("row_count");
932                 }
933             } catch (SQLException sqlException) {
934                 // don't die just because this failed
935             	originalFailure = sqlException;
936             } finally {
937                 if (rs != null) {
938                     try {
939                         rs.close();
940                     } catch (SQLException exc) {}
941                 }
942                 if (stmt != null) {
943                     try {
944                         stmt.close();
945                     } catch (SQLException exc) {}
946                 }
947             }
948         }
949 
950         // if we get here then we either didn't have custom SQL or it didn't work
951         try {
952             // '*' should work best for the majority of cases
953             return fetchNumRows("count(*)", false);
954         } catch (SQLException try2Exception) {
955             try {
956                 // except nested tables...try using '1' instead
957                 return fetchNumRows("count(1)", false);
958             } catch (SQLException try3Exception) {
959                 logger.warning("Unable to extract the number of rows for table " + getName() + ", using '-1'");
960                 if (originalFailure != null)
961                     logger.warning(originalFailure.toString());
962                 logger.warning(try2Exception.toString());
963                 logger.warning(try3Exception.toString());
964                 return -1;
965             }
966         }
967     }
968 
969     protected long fetchNumRows(String clause, boolean forceQuotes) throws SQLException {
970         PreparedStatement stmt = null;
971         ResultSet rs = null;
972         StringBuilder sql = new StringBuilder("select ");
973         sql.append(clause);
974         sql.append(" from ");
975         if (getSchema() != null) {
976             sql.append(getSchema());
977             sql.append('.');
978         }
979 
980         if (forceQuotes) {
981             String quote = db.getMetaData().getIdentifierQuoteString().trim();
982             sql.append(quote + getName() + quote);
983         } else
984             sql.append(db.getQuotedIdentifier(getName()));
985 
986         try {
987             stmt = db.getConnection().prepareStatement(sql.toString());
988             rs = stmt.executeQuery();
989             while (rs.next()) {
990                 return rs.getLong(1);
991             }
992             return -1;
993         } catch (SQLException exc) {
994             if (forceQuotes) // we tried with and w/o quotes...fail this attempt
995                 throw exc;
996 
997             return fetchNumRows(clause, true);
998         } finally {
999             if (rs != null)
1000                 rs.close();
1001             if (stmt != null)
1002                 stmt.close();
1003         }
1004     }
1005 
1006     /**
1007      * Update the table with the specified XML-derived metadata
1008      *
1009      * @param tableMeta
1010      */
1011     public void update(TableMeta tableMeta) {
1012         String newComments = tableMeta.getComments();
1013         if (newComments != null) {
1014             comments = newComments;
1015         }
1016 
1017         for (TableColumnMeta colMeta : tableMeta.getColumns()) {
1018             TableColumn col = getColumn(colMeta.getName());
1019             if (col == null) {
1020                 if (tableMeta.getRemoteSchema() == null) {
1021                     logger.warning("Unrecognized column '" + colMeta.getName() + "' for table '" + getName() + '\'');
1022                     continue;
1023                 }
1024 
1025                 col = addColumn(colMeta);
1026             }
1027 
1028             // update the column with the changes
1029             col.update(colMeta);
1030         }
1031     }
1032 
1033     /**
1034      * uses XML-based metadata.
1035      *
1036      * @param tableMeta
1037      * @param tables
1038      * @param remoteTables
1039      */
1040     public void connect(TableMeta tableMeta, Map<String, Table> tables, Map<String, Table> remoteTables) {
1041         for (TableColumnMeta colMeta : tableMeta.getColumns()) {
1042             TableColumn col = getColumn(colMeta.getName());
1043 
1044              // go thru the new foreign key defs and associate them with our columns
1045             for (ForeignKeyMeta fk : colMeta.getForeignKeys()) {
1046                 Table parent = fk.getRemoteSchema() == null ? tables.get(fk.getTableName())
1047                                                             : remoteTables.get(fk.getRemoteSchema() + '.' + fk.getTableName());
1048                 if (parent != null) {
1049                     TableColumn parentColumn = parent.getColumn(fk.getColumnName());
1050 
1051                     if (parentColumn == null) {
1052                         logger.warning(parent.getName() + '.' + fk.getColumnName() + " doesn't exist");
1053                     } else {
1054                         /**
1055                          * Merely instantiating a foreign key constraint ties it
1056                          * into its parent and child columns (& therefore their tables)
1057                          */
1058                         new ForeignKeyConstraint(parentColumn, col) {
1059                             @Override
1060                             public String getName() {
1061                                 return "Defined in XML";
1062                             }
1063                         };
1064                     }
1065                 } else {
1066                     logger.warning("Undefined table '" + fk.getTableName() + "' referenced by '" + getName() + '.' + col.getName() + '\'');
1067                 }
1068             }
1069         }
1070     }
1071 
1072     @Override
1073     public String toString() {
1074         return getName();
1075     }
1076 
1077     /**
1078      * Returns <code>true</code> if this table has no relationships
1079      *
1080      * @param withImpliedRelationships boolean
1081      * @return boolean
1082      */
1083     public boolean isOrphan(boolean withImpliedRelationships) {
1084         if (withImpliedRelationships)
1085             return getMaxParents() == 0 && getMaxChildren() == 0;
1086 
1087         for (TableColumn column : columns.values()) {
1088             for (TableColumn parentColumn : column.getParents()) {
1089                 if (!column.getParentConstraint(parentColumn).isImplied())
1090                     return false;
1091             }
1092             for (TableColumn childColumn : column.getChildren()) {
1093                 if (!column.getChildConstraint(childColumn).isImplied())
1094                     return false;
1095             }
1096         }
1097         return true;
1098     }
1099 
1100     /**
1101      * Compare this table to another table.
1102      * Results are based on 1: identity, 2: table name, 3: schema name<p/>
1103      *
1104      * This implementation was put in place to deal with analyzing multiple
1105      * schemas that contain identically named tables.
1106      *
1107      * @see {@link Comparable#compareTo(Object)}
1108      */
1109     public int compareTo(Table other) {
1110         if (other == this)  // fast way out
1111             return 0;
1112 
1113         int rc = getName().compareToIgnoreCase(other.getName());
1114         if (rc == 0) {
1115             // should only get here if we're dealing with cross-schema references (rare)
1116             String ours = getSchema();
1117             String theirs = other.getSchema();
1118             if (ours != null && theirs != null)
1119                 rc = ours.compareToIgnoreCase(theirs);
1120             else if (ours == null)
1121                 rc = -1;
1122             else
1123                 rc = 1;
1124         }
1125 
1126         return rc;
1127     }
1128 
1129     /**
1130      * Implementation of {@link Comparator} that sorts {@link TableColumn}s
1131      * by {@link TableColumn#getId() ID} (ignored if <code>null</code>)
1132      * followed by {@link TableColumn#getName() Name}.
1133      */
1134     private static class ByColumnIdComparator implements Comparator<TableColumn> {
1135         public int compare(TableColumn column1, TableColumn column2) {
1136             if (column1.getId() == null || column2.getId() == null)
1137                 return column1.getName().compareToIgnoreCase(column2.getName());
1138             if (column1.getId() instanceof Number)
1139                 return ((Number)column1.getId()).intValue() - ((Number)column2.getId()).intValue();
1140             return column1.getId().toString().compareToIgnoreCase(column2.getId().toString());
1141         }
1142     }
1143 }