001package edu.pdx.cs.joy.jdbc;
002
003import java.io.File;
004import java.sql.*;
005import java.util.Set;
006
007/**
008 * A command-line program that uses the JDBC DatabaseMetaData API to print
009 * information about the tables in an H2 database file.
010 */
011public class PrintH2DatabaseSchema {
012
013  private static final Set<String> uninterestingTablePrefixes = Set.of(
014    "CONSTANTS", "ENUM_VALUES", "INDEXES", "INDEX_COLUMNS", "INFORMATION_SCHEMA_CATALOG_NAME",
015    "IN_DOUBT", "LOCKS", "QUERY_STATISTICS", "RIGHTS", "ROLES", "SESSIONS", "SESSION_STATE",
016    "SETTINGS", "SYNONYMS", "USERS"
017  );
018
019  /**
020   * Prints information about all tables in the database.
021   *
022   * @param connection the database connection
023   * @throws SQLException if a database error occurs
024   */
025  private static void printDatabaseSchema(Connection connection) throws SQLException {
026    DatabaseMetaData metaData = connection.getMetaData();
027
028    System.out.println("=== Database Information ===");
029    System.out.println("Database Product: " + metaData.getDatabaseProductName());
030    System.out.println("Database Version: " + metaData.getDatabaseProductVersion());
031    System.out.println("Driver Name: " + metaData.getDriverName());
032    System.out.println("Driver Version: " + metaData.getDriverVersion());
033    System.out.println();
034
035    // Get all tables
036    System.out.println("=== Tables ===");
037    try (ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"})) {
038      boolean foundTables = false;
039
040      while (tables.next()) {
041        foundTables = true;
042        String tableName = tables.getString("TABLE_NAME");
043
044        if (tableNameIsNotInteresting(tableName)) {
045          continue; // Skip system or uninteresting tables
046        }
047
048        String tableType = tables.getString("TABLE_TYPE");
049        String remarks = tables.getString("REMARKS");
050
051        System.out.println("\nTable: " + tableName);
052        System.out.println("  Type: " + tableType);
053        if (remarks != null && !remarks.isEmpty()) {
054          System.out.println("  Remarks: " + remarks);
055        }
056
057        // Print columns for this table
058        printTableColumns(metaData, tableName);
059
060        // Print primary keys
061        printPrimaryKeys(metaData, tableName);
062
063        // Print foreign keys
064        printForeignKeys(metaData, tableName);
065
066        // Print indexes
067        printIndexes(metaData, tableName);
068      }
069
070      if (!foundTables) {
071        System.out.println("No tables found in the database.");
072      }
073    }
074  }
075
076  private static boolean tableNameIsNotInteresting(String tableName) {
077    return uninterestingTablePrefixes.stream().anyMatch(tableName::startsWith);
078  }
079
080  /**
081   * Prints information about columns in a table.
082   */
083  private static void printTableColumns(DatabaseMetaData metaData, String tableName) throws SQLException {
084    System.out.println("  Columns:");
085    try (ResultSet columns = metaData.getColumns(null, null, tableName, "%")) {
086      while (columns.next()) {
087        String columnName = columns.getString("COLUMN_NAME");
088        String columnType = columns.getString("TYPE_NAME");
089        int columnSize = columns.getInt("COLUMN_SIZE");
090        String nullable = columns.getString("IS_NULLABLE");
091        String defaultValue = columns.getString("COLUMN_DEF");
092
093        System.out.print("    - " + columnName + " " + columnType);
094        if (columnSize > 0) {
095          System.out.print("(" + columnSize + ")");
096        }
097        System.out.print(" [" + (nullable.equals("YES") ? "NULL" : "NOT NULL") + "]");
098        if (defaultValue != null) {
099          System.out.print(" DEFAULT " + defaultValue);
100        }
101        System.out.println();
102      }
103    }
104  }
105
106  /**
107   * Prints information about primary keys in a table.
108   */
109  private static void printPrimaryKeys(DatabaseMetaData metaData, String tableName) throws SQLException {
110    System.out.println("  Primary Keys:");
111    try (ResultSet primaryKeys = metaData.getPrimaryKeys(null, null, tableName)) {
112      boolean foundKeys = false;
113      while (primaryKeys.next()) {
114        foundKeys = true;
115        String columnName = primaryKeys.getString("COLUMN_NAME");
116        String pkName = primaryKeys.getString("PK_NAME");
117        int keySeq = primaryKeys.getInt("KEY_SEQ");
118
119        System.out.println("    - " + columnName + " (Key: " + pkName + ", Sequence: " + keySeq + ")");
120      }
121      if (!foundKeys) {
122        System.out.println("    None");
123      }
124    }
125  }
126
127  /**
128   * Prints information about foreign keys in a table.
129   */
130  private static void printForeignKeys(DatabaseMetaData metaData, String tableName) throws SQLException {
131    System.out.println("  Foreign Keys:");
132    try (ResultSet foreignKeys = metaData.getImportedKeys(null, null, tableName)) {
133      boolean foundKeys = false;
134      while (foreignKeys.next()) {
135        foundKeys = true;
136        String fkColumnName = foreignKeys.getString("FKCOLUMN_NAME");
137        String pkTableName = foreignKeys.getString("PKTABLE_NAME");
138        String pkColumnName = foreignKeys.getString("PKCOLUMN_NAME");
139        String fkName = foreignKeys.getString("FK_NAME");
140
141        System.out.println("    - " + fkColumnName + " -> " + pkTableName + "(" + pkColumnName + ")" +
142                          (fkName != null ? " [" + fkName + "]" : ""));
143      }
144      if (!foundKeys) {
145        System.out.println("    None");
146      }
147    }
148  }
149
150  /**
151   * Prints information about indexes in a table.
152   */
153  private static void printIndexes(DatabaseMetaData metaData, String tableName) throws SQLException {
154    System.out.println("  Indexes:");
155    try (ResultSet indexes = metaData.getIndexInfo(null, null, tableName, false, false)) {
156      boolean foundIndexes = false;
157      String lastIndexName = null;
158      StringBuilder indexColumns = new StringBuilder();
159
160      while (indexes.next()) {
161        String indexName = indexes.getString("INDEX_NAME");
162        String columnName = indexes.getString("COLUMN_NAME");
163        boolean nonUnique = indexes.getBoolean("NON_UNIQUE");
164
165        if (indexName == null) {
166          continue; // Skip table statistics
167        }
168
169        if (lastIndexName != null && !lastIndexName.equals(indexName)) {
170          // Print the previous index
171          System.out.println("    - " + lastIndexName + " (" + indexColumns + ")");
172          indexColumns.setLength(0);
173        }
174
175        if (indexColumns.length() > 0) {
176          indexColumns.append(", ");
177        }
178        indexColumns.append(columnName);
179        lastIndexName = indexName;
180        foundIndexes = true;
181      }
182
183      // Print the last index
184      if (lastIndexName != null) {
185        System.out.println("    - " + lastIndexName + " (" + indexColumns + ")");
186      }
187
188      if (!foundIndexes) {
189        System.out.println("    None");
190      }
191    }
192  }
193
194  /**
195   * Main method that takes a database file path and prints the schema.
196   *
197   * @param args command line arguments where args[0] is the path to the H2 database file
198   * @throws SQLException if a database error occurs
199   */
200  public static void main(String[] args) throws SQLException {
201    if (args.length < 1) {
202      System.err.println("Missing database file path argument");
203      System.err.println("Usage: java PrintH2DatabaseSchema <database-file-path>");
204      return;
205    }
206
207    String dbFilePath = args[0];
208    File dbFile = new File(dbFilePath);
209
210    System.out.println("Reading schema from H2 database: " + dbFile.getAbsolutePath());
211    System.out.println();
212
213    try (Connection connection = H2DatabaseHelper.createFileBasedConnection(dbFile)) {
214      printDatabaseSchema(connection);
215    }
216  }
217}