blob: 0e69eb284792cabe8ac012dd911113aec4def8d4 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.oozie.command;
import org.apache.commons.collections.CollectionUtils;
import org.apache.oozie.BinaryBlob;
import org.apache.oozie.BundleActionBean;
import org.apache.oozie.BundleJobBean;
import org.apache.oozie.CoordinatorActionBean;
import org.apache.oozie.CoordinatorJobBean;
import org.apache.oozie.SLAEventBean;
import org.apache.oozie.StringBlob;
import org.apache.oozie.WorkflowActionBean;
import org.apache.oozie.WorkflowJobBean;
import org.apache.oozie.client.rest.JsonBean;
import org.apache.oozie.client.rest.JsonSLAEvent;
import org.apache.oozie.service.SchemaCheckerService;
import org.apache.oozie.service.Services;
import org.apache.oozie.sla.SLARegistrationBean;
import org.apache.oozie.sla.SLASummaryBean;
import org.apache.oozie.util.Pair;
import org.apache.oozie.util.XLog;
import org.apache.oozie.util.db.CompositeIndex;
import org.apache.openjpa.persistence.jdbc.Index;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class SchemaCheckXCommand extends XCommand<Void> {
private XLog LOG = XLog.getLog(SchemaCheckXCommand.class);
private String dbType;
private String url;
private String user;
private String pass;
private boolean ignoreExtras;
public SchemaCheckXCommand(String dbType, String url, String user, String pass, boolean ignoreExtras) {
super("schema-check", "schema-check", 0);
this.dbType = dbType;
this.url = url;
this.user = user;
this.pass = pass;
this.ignoreExtras = ignoreExtras;
}
@Override
protected Void execute() throws CommandException {
Connection conn = null;
LOG.info("About to check database schema");
Date startTime = new Date();
boolean problem = false;
try {
conn = DriverManager.getConnection(url, user, pass);
String catalog = conn.getCatalog();
DatabaseMetaData metaData = conn.getMetaData();
Map<String, Class<? extends JsonBean>> tableClasses = new HashMap<String, Class<? extends JsonBean>>();
tableClasses.put(getTableName(BundleActionBean.class), BundleActionBean.class);
tableClasses.put(getTableName(BundleJobBean.class), BundleJobBean.class);
tableClasses.put(getTableName(CoordinatorActionBean.class), CoordinatorActionBean.class);
tableClasses.put(getTableName(CoordinatorJobBean.class), CoordinatorJobBean.class);
tableClasses.put(getTableName(JsonSLAEvent.class), JsonSLAEvent.class);
tableClasses.put(getTableName(SLARegistrationBean.class), SLARegistrationBean.class);
tableClasses.put(getTableName(SLASummaryBean.class), SLASummaryBean.class);
tableClasses.put(getTableName(WorkflowActionBean.class), WorkflowActionBean.class);
tableClasses.put(getTableName(WorkflowJobBean.class), WorkflowJobBean.class);
boolean tableProblem = checkTables(metaData, catalog, tableClasses.keySet());
problem = problem | tableProblem;
if (!tableProblem) {
for (Map.Entry<String, Class<? extends JsonBean>> table : tableClasses.entrySet()) {
TableInfo ti = new TableInfo(table.getValue(), dbType);
boolean columnProblem = checkColumns(metaData, catalog, table.getKey(), ti.columnTypes);
problem = problem | columnProblem;
if (!columnProblem) {
boolean primaryKeyProblem = checkPrimaryKey(metaData, catalog, table.getKey(), ti.primaryKeyColumn);
problem = problem | primaryKeyProblem;
boolean indexProblem = checkIndexes(metaData, catalog, table.getKey(), ti.indexedColumns);
problem = problem | indexProblem;
}
}
}
if (problem) {
LOG.error("Database schema is BAD! Check previous error log messages for details");
} else {
LOG.info("Database schema is GOOD");
}
} catch (SQLException sqle) {
LOG.error("An Exception occurred while talking to the database: " + sqle.getMessage(), sqle);
problem = true;
} finally {
if (conn != null) {
try {
conn.close();
} catch (Exception e) {
LOG.error("An Exception occurred while disconnecting from the database: " + e.getMessage(), e);
}
}
Services.get().get(SchemaCheckerService.class).updateInstrumentation(problem, startTime);
}
return null;
}
private boolean checkTables(DatabaseMetaData metaData, String catalog, final Collection<String> expectedTablesRaw)
throws SQLException {
boolean problem = false;
Set<String> expectedTables = new HashSet<String>(expectedTablesRaw);
expectedTables.add(caseTableName("oozie_sys"));
expectedTables.add(caseTableName("openjpa_sequence_table"));
expectedTables.add(caseTableName("validate_conn"));
// Oracle returns > 1000 tables if we don't have the schema "OOZIE"; MySQL and Postgres don't want this
String schema = null;
if (dbType.equals("oracle")) {
schema = "OOZIE";
}
ResultSet rs = metaData.getTables(catalog, schema, null, new String[]{"TABLE"});
Set<String> foundTables = new HashSet<String>();
while (rs.next()) {
String tabName = rs.getString("TABLE_NAME");
if (tabName != null) {
foundTables.add(tabName);
}
}
Collection missingTables = CollectionUtils.subtract(expectedTables, foundTables);
if (!missingTables.isEmpty()) {
LOG.error("Found [{0}] missing tables: {1}", missingTables.size(), Arrays.toString(missingTables.toArray()));
problem = true;
} else if (LOG.isDebugEnabled()) {
LOG.debug("No missing tables found: {0}", Arrays.toString(expectedTables.toArray()));
}
if (!ignoreExtras) {
Collection extraTables = CollectionUtils.subtract(foundTables, expectedTables);
if (!extraTables.isEmpty()) {
LOG.error("Found [{0}] extra tables: {1}", extraTables.size(), Arrays.toString(extraTables.toArray()));
problem = true;
} else {
LOG.debug("No extra tables found");
}
}
return problem;
}
private boolean checkColumns(DatabaseMetaData metaData, String catalog, String table,
Map<String, Integer> expectedColumnTypes) throws SQLException {
boolean problem = false;
Map<String, Pair<Integer, String>> foundColumns = new HashMap<String, Pair<Integer, String>>();
ResultSet rs = metaData.getColumns(catalog, null, table, null);
while (rs.next()) {
String colName = rs.getString("COLUMN_NAME");
Integer dataType = rs.getInt("DATA_TYPE");
String colDef = rs.getString("COLUMN_DEF");
if (colName != null) {
foundColumns.put(colName, new Pair<Integer, String>(dataType, colDef));
}
}
Collection missingColumns = CollectionUtils.subtract(expectedColumnTypes.keySet(), foundColumns.keySet());
if (!missingColumns.isEmpty()) {
LOG.error("Found [{0}] missing columns in table [{1}]: {2}",
missingColumns.size(), table, Arrays.toString(missingColumns.toArray()));
problem = true;
} else {
for (Map.Entry<String, Integer> ent : expectedColumnTypes.entrySet()) {
if (!foundColumns.get(ent.getKey()).getFirst().equals(ent.getValue())) {
LOG.error("Expected column [{0}] in table [{1}] to have type [{2}], but found type [{3}]",
ent.getKey(), table, getSQLTypeFromInt(ent.getValue()),
getSQLTypeFromInt(foundColumns.get(ent.getKey()).getFirst()));
problem = true;
} else if (foundColumns.get(ent.getKey()).getSecond() != null) {
LOG.error("Expected column [{0}] in table [{1}] to have default value [NULL], but found default vale [{2}]",
ent.getKey(), table, foundColumns.get(ent.getKey()).getSecond());
problem = true;
} else {
LOG.debug("Found column [{0}] in table [{1}] with type [{2}] and default value [NULL]",
ent.getKey(), table, getSQLTypeFromInt(ent.getValue()));
}
}
}
if (!ignoreExtras) {
Collection extraColumns = CollectionUtils.subtract(foundColumns.keySet(), expectedColumnTypes.keySet());
if (!extraColumns.isEmpty()) {
LOG.error("Found [{0}] extra columns in table [{1}]: {2}",
extraColumns.size(), table, Arrays.toString(extraColumns.toArray()));
problem = true;
} else {
LOG.debug("No extra columns found in table [{0}]", table);
}
}
return problem;
}
private boolean checkPrimaryKey(DatabaseMetaData metaData, String catalog, String table, String expectedPrimaryKeyColumn)
throws SQLException {
boolean problem = false;
ResultSet rs = metaData.getPrimaryKeys(catalog, null, table);
if (!rs.next()) {
LOG.error("Expected column [{0}] to be the primary key in table [{1}], but none were found",
expectedPrimaryKeyColumn, table);
problem = true;
} else {
String foundPrimaryKeyColumn = rs.getString("COLUMN_NAME");
if (!foundPrimaryKeyColumn.equals(expectedPrimaryKeyColumn)) {
LOG.error("Expected column [{0}] to be the primary key in table [{1}], but found column [{2}] instead",
expectedPrimaryKeyColumn, table, foundPrimaryKeyColumn);
problem = true;
} else {
LOG.debug("Found column [{0}] to be the primary key in table [{1}]", expectedPrimaryKeyColumn, table);
}
}
return problem;
}
private boolean checkIndexes(DatabaseMetaData metaData, String catalog, String table, Set<String> expectedIndexedColumns)
throws SQLException {
boolean problem = false;
Set<String> foundIndexedColumns = new HashSet<String>();
ResultSet rs = metaData.getIndexInfo(catalog, null, table, false, true);
while (rs.next()) {
String colName = rs.getString("COLUMN_NAME");
String indexName = rs.getString("INDEX_NAME");
final boolean isExtraIndexedColumn = !CompositeIndex.find(indexName) && colName != null;
if (isExtraIndexedColumn) {
foundIndexedColumns.add(colName);
}
}
Collection missingIndexColumns = CollectionUtils.subtract(expectedIndexedColumns, foundIndexedColumns);
if (!missingIndexColumns.isEmpty()) {
LOG.error("Found [{0}] missing indexes for columns in table [{1}]: {2}",
missingIndexColumns.size(), table, Arrays.toString(missingIndexColumns.toArray()));
problem = true;
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("No missing indexes found in table [{0}]: {1}",
table, Arrays.toString(expectedIndexedColumns.toArray()));
}
}
if (!ignoreExtras) {
Collection extraIndexColumns = CollectionUtils.subtract(foundIndexedColumns, expectedIndexedColumns);
if (!extraIndexColumns.isEmpty()) {
LOG.error("Found [{0}] extra indexes for columns in table [{1}]: {2}",
extraIndexColumns.size(), table, Arrays.toString(extraIndexColumns.toArray()));
problem = true;
} else {
LOG.debug("No extra indexes found in table [{0}]", table);
}
}
return problem;
}
private String getTableName(Class<? extends JsonBean> clazz) {
Table tabAnn = clazz.getAnnotation(Table.class);
if (tabAnn != null) {
return caseTableName(tabAnn.name());
}
return null;
}
private String caseTableName(String name) {
// MySQL and Oracle wants table names in all caps
if (dbType.equals("mysql") || dbType.equals("oracle")) {
return name.toUpperCase();
}
// Postgres wants table names in all lowers
if (dbType.equals("postgresql")) {
return name.toLowerCase();
}
return name;
}
private String getSQLTypeFromInt(int t) {
switch (t) {
case Types.BIT:
return "BIT";
case Types.TINYINT:
return "TINYINT";
case Types.SMALLINT:
return "SMALLINT";
case Types.INTEGER:
return "INTEGER";
case Types.BIGINT:
return "BIGINT";
case Types.FLOAT:
return "FLOAT";
case Types.REAL:
return "REAL";
case Types.DOUBLE:
return "DOUBLE";
case Types.NUMERIC:
return "NUMERIC";
case Types.DECIMAL:
return "DECIMAL";
case Types.CHAR:
return "CHAR";
case Types.VARCHAR:
return "VARCHAR";
case Types.LONGVARCHAR:
return "LONGVARCHAR";
case Types.DATE:
return "DATE";
case Types.TIME:
return "TIME";
case Types.TIMESTAMP:
return "TIMESTAMP";
case Types.BINARY:
return "BINARY";
case Types.VARBINARY:
return "VARBINARY";
case Types.LONGVARBINARY:
return "LONGVARBINARY";
case Types.NULL:
return "NULL";
case Types.OTHER:
return "OTHER";
case Types.JAVA_OBJECT:
return "JAVA_OBJECT";
case Types.DISTINCT:
return "DISTINCT";
case Types.STRUCT:
return "STRUCT";
case Types.ARRAY:
return "ARRAY";
case Types.BLOB:
return "BLOB";
case Types.CLOB:
return "CLOB";
case Types.REF:
return "REF";
case Types.DATALINK:
return "DATALINK";
case Types.BOOLEAN:
return "BOOLEAN";
case Types.ROWID:
return "ROWID";
case Types.NCHAR:
return "NCHAR";
case Types.NVARCHAR:
return "NVARCHAR";
case Types.LONGNVARCHAR:
return "LONGNVARCHAR";
case Types.NCLOB:
return "NCLOB";
case Types.SQLXML:
return "SQLXML";
default:
return "unknown";
}
}
private static class TableInfo {
String primaryKeyColumn;
Map<String, Integer> columnTypes;
Set<String> indexedColumns;
public TableInfo(Class<? extends JsonBean> clazz, String dbType) {
columnTypes = new HashMap<String, Integer>();
indexedColumns = new HashSet<String>();
populate(clazz, dbType);
// The "SLA_EVENTS" table is made up of two classes (JsonSLAEvent and SLAEventBean), and the reflection doesn't pick up
// from both automatically, so we have to manually do this
if (clazz.equals(JsonSLAEvent.class)) {
populate(SLAEventBean.class, dbType);
}
}
private void populate(Class<? extends JsonBean> clazz, String dbType) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Column colAnn = field.getAnnotation(Column.class);
if (colAnn != null) {
String name = caseColumnName(colAnn.name(), dbType);
boolean isLob = (field.getAnnotation(Lob.class) != null);
Integer type = getSQLType(field.getType(), isLob, dbType);
columnTypes.put(name, type);
boolean isIndex = (field.getAnnotation(Index.class) != null);
if (isIndex) {
indexedColumns.add(name);
}
boolean isPrimaryKey = (field.getAnnotation(Id.class) != null);
if (isPrimaryKey) {
indexedColumns.add(name);
primaryKeyColumn = name;
}
} else {
// Some Id fields don't have an @Column annotation
Id idAnn = field.getAnnotation(Id.class);
if (idAnn != null) {
String name = caseColumnName(field.getName(), dbType);
boolean isLob = (field.getAnnotation(Lob.class) != null);
Integer type = getSQLType(field.getType(), isLob, dbType);
columnTypes.put(name, type);
indexedColumns.add(name);
primaryKeyColumn = name;
}
}
}
DiscriminatorColumn discAnn = clazz.getAnnotation(DiscriminatorColumn.class);
if (discAnn != null) {
String name = caseColumnName(discAnn.name(), dbType);
Integer type = getSQLType(discAnn.discriminatorType());
columnTypes.put(name, type);
indexedColumns.add(name);
}
}
private static Integer getSQLType(Class<?> clazz, boolean isLob, String dbType) {
if (clazz.equals(String.class)) {
if (dbType.equals("mysql") && isLob) {
return Types.LONGVARCHAR;
}
if (dbType.equals("oracle") && isLob) {
return Types.CLOB;
}
return Types.VARCHAR;
}
if (clazz.equals(StringBlob.class) || clazz.equals(BinaryBlob.class)) {
if (dbType.equals("mysql")) {
return Types.LONGVARBINARY;
}
if (dbType.equals("oracle")) {
return Types.BLOB;
}
return Types.BINARY;
}
if (clazz.equals(Timestamp.class)) {
return Types.TIMESTAMP;
}
if (clazz.equals(int.class)) {
if (dbType.equals("oracle")) {
return Types.DECIMAL;
}
return Types.INTEGER;
}
if (clazz.equals(long.class)) {
if (dbType.equals("oracle")) {
return Types.DECIMAL;
}
return Types.BIGINT;
}
if (clazz.equals(byte.class)) {
if (dbType.equals("mysql")) {
return Types.TINYINT;
}
if (dbType.equals("oracle")) {
return Types.DECIMAL;
}
return Types.SMALLINT;
}
return null;
}
private static Integer getSQLType(DiscriminatorType discType) {
switch (discType) {
case STRING:
return Types.VARCHAR;
case CHAR:
return Types.CHAR;
case INTEGER:
return Types.INTEGER;
}
return null;
}
private static String caseColumnName(String name, String dbType) {
// Oracle wants column names in all caps
if (dbType.equals("oracle")) {
return name.toUpperCase();
}
// Postgres and MySQL want column names in all lowers
if (dbType.equals("postgresql") || dbType.equals("mysql")) {
return name.toLowerCase();
}
return name;
}
}
@Override
protected void loadState() throws CommandException {
}
@Override
protected void verifyPrecondition() throws CommandException, PreconditionException {
}
@Override
protected boolean isLockRequired() {
return false;
}
@Override
public String getEntityKey() {
return null;
}
}