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;
20  
21  import java.sql.DatabaseMetaData;
22  import java.sql.ResultSet;
23  import java.sql.ResultSetMetaData;
24  import java.sql.SQLException;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.TreeMap;
35  import java.util.TreeSet;
36  import java.util.logging.Level;
37  import java.util.logging.Logger;
38  import java.util.regex.Pattern;
39  import net.sourceforge.schemaspy.model.ForeignKeyConstraint;
40  import net.sourceforge.schemaspy.model.ImpliedForeignKeyConstraint;
41  import net.sourceforge.schemaspy.model.RailsForeignKeyConstraint;
42  import net.sourceforge.schemaspy.model.Table;
43  import net.sourceforge.schemaspy.model.TableColumn;
44  import net.sourceforge.schemaspy.model.TableIndex;
45  import net.sourceforge.schemaspy.util.Inflection;
46  
47  public class DbAnalyzer {
48      public static List<ImpliedForeignKeyConstraint> getImpliedConstraints(Collection<Table> tables) {
49          List<TableColumn> columnsWithoutParents = new ArrayList<TableColumn>();
50          Map<TableColumn, Table> allPrimaries = new TreeMap<TableColumn, Table>(new Comparator<TableColumn>() {
51              public int compare(TableColumn column1, TableColumn column2) {
52                  int rc = column1.getName().compareToIgnoreCase(column2.getName());
53                  if (rc == 0)
54                      rc = column1.getType().compareToIgnoreCase(column2.getType());
55                  if (rc == 0)
56                      rc = column1.getLength() - column2.getLength();
57                  return rc;
58              }
59          });
60  
61          int duplicatePrimaries = 0;
62  
63          // gather all the primary key columns and columns without parents
64          for (Table table : tables) {
65              List<TableColumn> tablePrimaries = table.getPrimaryColumns();
66              if (tablePrimaries.size() == 1) { // can't match up multiples...yet...
67                  for (TableColumn primary : tablePrimaries) {
68                      if (primary.allowsImpliedChildren() &&
69                          allPrimaries.put(primary, table) != null)
70                          ++duplicatePrimaries;
71                  }
72              }
73  
74              for (TableColumn column : table.getColumns()) {
75                  if (!column.isForeignKey() && column.allowsImpliedParents())
76                      columnsWithoutParents.add(column);
77              }
78          }
79  
80          // if more than half of the tables have the same primary key then
81          // it's most likely a database where primary key names aren't unique
82          // (e.g. they all have a primary key named 'ID')
83          if (duplicatePrimaries > allPrimaries.size()) // bizarre logic, but it does approximately what we need
84              return new ArrayList<ImpliedForeignKeyConstraint>();
85  
86          sortColumnsByTable(columnsWithoutParents);
87  
88          List<ImpliedForeignKeyConstraint> impliedConstraints = new ArrayList<ImpliedForeignKeyConstraint>();
89          for (TableColumn childColumn : columnsWithoutParents) {
90              Table primaryTable = allPrimaries.get(childColumn);
91              if (primaryTable != null && primaryTable != childColumn.getTable()) {
92                  TableColumn parentColumn = primaryTable.getColumn(childColumn.getName());
93                  // make sure the potential child->parent relationships isn't already a
94                  // parent->child relationship
95                  if (parentColumn.getParentConstraint(childColumn) == null) {
96                      // ok, we've found a potential relationship with a column matches a primary
97                      // key column in another table and isn't already related to that column
98                      impliedConstraints.add(new ImpliedForeignKeyConstraint(parentColumn, childColumn));
99                  }
100             }
101         }
102 
103         return impliedConstraints;
104     }
105 
106     /**
107      * Ruby on Rails-based databases typically have no real referential integrity
108      * constraints.  Instead they have a somewhat unusual way of associating
109      * columns to primary keys.<p>
110      *
111      * Basically all tables have a primary key named <code>ID</code>.
112      * All tables are named plural names.
113      * The columns that logically reference that <code>ID</code> are the singular
114      * form of the table name suffixed with <code>_ID</code>.<p>
115      *
116      * A side-effect of calling this method is that the returned collection of
117      * constraints will be "tied into" the associated tables.
118      *
119      * @param tables
120      * @return List of {@link RailsForeignKeyConstraint}s
121      */
122     public static List<RailsForeignKeyConstraint> getRailsConstraints(Map<String, Table> tables) {
123         List<RailsForeignKeyConstraint> railsConstraints = new ArrayList<RailsForeignKeyConstraint>(tables.size());
124 
125         // iterate thru each column in each table looking for columns that
126         // match Rails naming conventions
127         for (Table table : tables.values()) {
128             for (TableColumn column : table.getColumns()) {
129             	String columnName = column.getName().toLowerCase();
130                 if (!column.isForeignKey() && column.allowsImpliedParents() && columnName.endsWith("_id")) {
131                     String singular = columnName.substring(0, columnName.length() - 3);
132                     String primaryTableName = Inflection.pluralize(singular);
133                     Table primaryTable = tables.get(primaryTableName);
134                     if (primaryTable != null) {
135                         TableColumn primaryColumn = primaryTable.getColumn("ID");
136                         if (primaryColumn != null) {
137                             railsConstraints.add(new RailsForeignKeyConstraint(primaryColumn, column));
138                         }
139                     }
140                 }
141             }
142         }
143 
144         return railsConstraints;
145     }
146 
147     /**
148      * Returns a <code>List</code> of all of the <code>ForeignKeyConstraint</code>s
149      * used by the specified tables.
150      *
151      * @param tables Collection
152      * @return List
153      */
154     public static List<ForeignKeyConstraint> getForeignKeyConstraints(Collection<Table> tables) {
155         List<ForeignKeyConstraint> constraints = new ArrayList<ForeignKeyConstraint>();
156 
157         for (Table table : tables) {
158             constraints.addAll(table.getForeignKeys());
159         }
160 
161         return constraints;
162     }
163 
164     public static List<Table> getOrphans(Collection<Table> tables) {
165         List<Table> orphans = new ArrayList<Table>();
166 
167         for (Table table : tables) {
168             if (table.isOrphan(false)) {
169                 orphans.add(table);
170             }
171         }
172 
173         return sortTablesByName(orphans);
174     }
175 
176     /**
177      * Return a list of <code>TableColumn</code>s that are both nullable
178      * and have an index that specifies that they must be unique (a rather strange combo).
179      */
180     public static List<TableColumn> getMustBeUniqueNullableColumns(Collection<Table> tables) {
181         List<TableColumn> uniqueNullables = new ArrayList<TableColumn>();
182 
183         for (Table table : tables) {
184             for (TableIndex index : table.getIndexes()) {
185                 if (index.isUniqueNullable()) {
186                     uniqueNullables.addAll(index.getColumns());
187                 }
188             }
189         }
190 
191         return sortColumnsByTable(uniqueNullables);
192     }
193 
194     /**
195      * Return a list of <code>Table</code>s that have neither an index nor a primary key.
196      */
197     public static List<Table> getTablesWithoutIndexes(Collection<Table> tables) {
198         List<Table> withoutIndexes = new ArrayList<Table>();
199 
200         for (Table table : tables) {
201             if (!table.isView() && table.getIndexes().size() == 0)
202                 withoutIndexes.add(table);
203         }
204 
205         return sortTablesByName(withoutIndexes);
206     }
207 
208     public static List<Table> getTablesWithIncrementingColumnNames(Collection<Table> tables) {
209         List<Table> denormalizedTables = new ArrayList<Table>();
210 
211         for (Table table : tables) {
212             Map<String, Long> columnPrefixes = new HashMap<String, Long>();
213 
214             for (TableColumn column : table.getColumns()) {
215                 // search for columns that start with the same prefix
216                 // and end in an incrementing number
217 
218                 String columnName = column.getName();
219                 String numbers = null;
220                 for (int i = columnName.length() - 1; i > 0; --i) {
221                     if (Character.isDigit(columnName.charAt(i))) {
222                         numbers = String.valueOf(columnName.charAt(i)) + (numbers == null ? "" : numbers);
223                     } else {
224                         break;
225                     }
226                 }
227 
228                 // attempt to detect where they had an existing column
229                 // and added a "column2" type of column (we'll call this one "1")
230                 if (numbers == null) {
231                     numbers = "1";
232                     columnName = columnName + numbers;
233                 }
234 
235                 // see if we've already found a column with the same prefix
236                 // that had a numeric suffix +/- 1.
237                 String prefix = columnName.substring(0, columnName.length() - numbers.length());
238                 long numeric = Long.parseLong(numbers);
239                 Long existing = columnPrefixes.get(prefix);
240                 if (existing != null && Math.abs(existing.longValue() - numeric) == 1) {
241                     // found one so add it to our list and stop evaluating this table
242                     denormalizedTables.add(table);
243                     break;
244                 }
245                 columnPrefixes.put(prefix, new Long(numeric));
246             }
247         }
248 
249         return sortTablesByName(denormalizedTables);
250     }
251 
252     public static List<Table> getTablesWithOneColumn(Collection<Table> tables) {
253         List<Table> singleColumnTables = new ArrayList<Table>();
254 
255         for (Table table : tables) {
256             if (table.getColumns().size() == 1)
257                 singleColumnTables.add(table);
258         }
259 
260         return sortTablesByName(singleColumnTables);
261     }
262 
263     public static List<Table> sortTablesByName(List<Table> tables) {
264         Collections.sort(tables, new Comparator<Table>() {
265             public int compare(Table table1, Table table2) {
266                 return table1.compareTo(table2);
267             }
268         });
269 
270         return tables;
271     }
272 
273     public static List<TableColumn> sortColumnsByTable(List<TableColumn> columns) {
274         Collections.sort(columns, new Comparator<TableColumn>() {
275             public int compare(TableColumn column1, TableColumn column2) {
276                 int rc = column1.getTable().compareTo(column2.getTable());
277                 if (rc == 0)
278                     rc = column1.getName().compareToIgnoreCase(column2.getName());
279                 return rc;
280             }
281         });
282 
283         return columns;
284     }
285 
286     /**
287      * Returns a list of columns that have the word "NULL" or "null" as their default value
288      * instead of the likely candidate value null.
289      *
290      * @param tables Collection
291      * @return List
292      */
293     public static List<TableColumn> getDefaultNullStringColumns(Collection<Table> tables) {
294         List<TableColumn> defaultNullStringColumns = new ArrayList<TableColumn>();
295 
296         for (Table table : tables) {
297             for (TableColumn column : table.getColumns()) {
298                 Object defaultValue = column.getDefaultValue();
299                 if (defaultValue != null && defaultValue instanceof String) {
300                     String defaultString = defaultValue.toString();
301                     if (defaultString.trim().equalsIgnoreCase("null")) {
302                         defaultNullStringColumns.add(column);
303                     }
304                 }
305             }
306         }
307 
308         return sortColumnsByTable(defaultNullStringColumns);
309     }
310 
311     /**
312      * getSchemas - returns a List of schema names (Strings)
313      *
314      * @param meta DatabaseMetaData
315      */
316     public static List<String> getSchemas(DatabaseMetaData meta) throws SQLException {
317         List<String> schemas = new ArrayList<String>();
318 
319         ResultSet rs = meta.getSchemas();
320         while (rs.next()) {
321             schemas.add(rs.getString("TABLE_SCHEM"));
322         }
323         rs.close();
324 
325         return schemas;
326     }
327 
328     /**
329      * getSchemas - returns a List of schema names (Strings) that contain tables
330      *
331      * @param meta DatabaseMetaData
332      */
333     public static List<String> getPopulatedSchemas(DatabaseMetaData meta) throws SQLException {
334         return getPopulatedSchemas(meta, ".*");
335     }
336 
337     /**
338      * getSchemas - returns a List of schema names (Strings) that contain tables and
339      * match the <code>schemaSpec</code> regular expression
340      *
341      * @param meta DatabaseMetaData
342      */
343     public static List<String> getPopulatedSchemas(DatabaseMetaData meta, String schemaSpec) throws SQLException {
344         Set<String> schemas = new TreeSet<String>(); // alpha sorted
345         Pattern schemaRegex = Pattern.compile(schemaSpec);
346         Logger logger = Logger.getLogger(DbAnalyzer.class.getName());
347         boolean logging = logger.isLoggable(Level.FINE);
348 
349         Iterator<String> iter = getSchemas(meta).iterator();
350         while (iter.hasNext()) {
351             String schema = iter.next().toString();
352             if (schemaRegex.matcher(schema).matches()) {
353                 ResultSet rs = null;
354                 try {
355                     rs = meta.getTables(null, schema, "%", null);
356                     if (rs.next()) {
357                         if (logging)
358                             logger.fine("Including schema " + schema +
359                                         ": matches + \"" + schemaRegex + "\" and contains tables");
360                         schemas.add(schema);
361                     } else {
362                         if (logging)
363                             logger.fine("Excluding schema " + schema +
364                                         ": matches \"" + schemaRegex + "\" but contains no tables");
365                     }
366                 } catch (SQLException ignore) {
367                 } finally {
368                     if (rs != null)
369                         rs.close();
370                 }
371             } else {
372                 if (logging)
373                     logger.fine("Excluding schema " + schema +
374                                 ": doesn't match \"" + schemaRegex + '"');
375             }
376         }
377 
378         return new ArrayList<String>(schemas);
379     }
380 
381     /**
382      * For debugging/analyzing result sets
383      * @param rs ResultSet
384      * @throws SQLException
385      */
386     public static void dumpResultSetRow(ResultSet rs, String description) throws SQLException {
387         ResultSetMetaData meta = rs.getMetaData();
388         int numColumns = meta.getColumnCount();
389         System.out.println(numColumns + " columns of " + description + ":");
390         for (int i = 1; i <= numColumns; ++i) {
391             System.out.print(meta.getColumnLabel(i));
392             System.out.print(": ");
393             System.out.print(String.valueOf(rs.getString(i)));
394             System.out.print("\t");
395         }
396         System.out.println();
397     }
398 }