DatabaseIntrospector.java
/*
* Copyright 2006-2026 the original author or authors.
*
* Licensed 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
*
* https://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.mybatis.generator.internal.db;
import static org.mybatis.generator.internal.util.StringUtility.composeFullyQualifiedTableName;
import static org.mybatis.generator.internal.util.StringUtility.isTrue;
import static org.mybatis.generator.internal.util.StringUtility.stringContainsSQLWildcard;
import static org.mybatis.generator.internal.util.StringUtility.stringContainsSpace;
import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
import static org.mybatis.generator.internal.util.messages.Messages.getString;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Matcher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mybatis.generator.api.FullyQualifiedTable;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.JavaTypeResolver;
import org.mybatis.generator.api.KnownRuntime;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.JavaReservedWords;
import org.mybatis.generator.config.ColumnOverride;
import org.mybatis.generator.config.Context;
import org.mybatis.generator.config.GeneratedKey;
import org.mybatis.generator.config.PropertyRegistry;
import org.mybatis.generator.config.TableConfiguration;
import org.mybatis.generator.internal.ObjectFactory;
import org.mybatis.generator.internal.PluginAggregator;
import org.mybatis.generator.internal.util.JavaBeansUtil;
public class DatabaseIntrospector {
private final DatabaseMetaData databaseMetaData;
private final JavaTypeResolver javaTypeResolver;
private final List<String> warnings = new ArrayList<>();
private final Context context;
private final Log logger;
public DatabaseIntrospector(Context context, DatabaseMetaData databaseMetaData,
JavaTypeResolver javaTypeResolver) {
this.context = context;
this.databaseMetaData = databaseMetaData;
this.javaTypeResolver = javaTypeResolver;
logger = LogFactory.getLog(getClass());
}
public List<String> getWarnings() {
return Collections.unmodifiableList(warnings);
}
private void calculatePrimaryKey(FullyQualifiedTable table, IntrospectedTable introspectedTable) {
try (ResultSet rs = databaseMetaData.getPrimaryKeys(
table.getIntrospectedCatalog().orElse(null),
table.getIntrospectedSchema().orElse(null),
table.getIntrospectedTableName())) {
// keep primary columns in key sequence order
Map<Short, String> keyColumns = new TreeMap<>();
while (rs.next()) {
String columnName = rs.getString("COLUMN_NAME"); //$NON-NLS-1$
short keySeq = rs.getShort("KEY_SEQ"); //$NON-NLS-1$
keyColumns.put(keySeq, columnName);
}
for (String columnName : keyColumns.values()) {
introspectedTable.addPrimaryKeyColumn(columnName);
}
} catch (SQLException e) {
warnings.add(getString("Warning.15")); //$NON-NLS-1$
}
}
private void reportIntrospectionWarnings(IntrospectedTable introspectedTable,
TableConfiguration tableConfiguration, FullyQualifiedTable table) {
// make sure that every column listed in column overrides
// actually exists in the table
for (ColumnOverride columnOverride : tableConfiguration.getColumnOverrides()) {
if (introspectedTable.getColumn(columnOverride.getColumnName()).isEmpty()) {
warnings.add(getString("Warning.3", columnOverride.getColumnName(), table.toString()));//$NON-NLS-1$
}
}
// make sure that every column listed in ignored columns
// actually exists in the table
for (String string : tableConfiguration.getIgnoredColumnsInError()) {
warnings.add(getString("Warning.4", string, table.toString())); //$NON-NLS-1$
}
tableConfiguration.getGeneratedKey().ifPresent(generatedKey -> {
if (introspectedTable.getColumn(generatedKey.getColumn()).isEmpty()) {
if (generatedKey.isIdentity()) {
warnings.add(getString("Warning.5", generatedKey.getColumn(), table.toString())); //$NON-NLS-1$
} else {
warnings.add(getString("Warning.6", generatedKey.getColumn(), table.toString())); //$NON-NLS-1$
}
}
});
for (IntrospectedColumn ic : introspectedTable.getAllColumns()) {
if (JavaReservedWords.containsWord(ic.getJavaProperty())) {
warnings.add(getString("Warning.26", ic.getActualColumnName(), table.toString())); //$NON-NLS-1$
}
}
}
/**
* Returns a List of IntrospectedTable elements that matches the specified table configuration.
*
* @param tc
* the table configuration
* @return a list of introspected tables
* @throws SQLException
* if any errors in introspection
*/
public List<IntrospectedTable> introspectTables(TableConfiguration tc, KnownRuntime knownRuntime,
PluginAggregator pluginAggregator) throws SQLException {
// get the raw columns from the DB
Map<ActualTableName, List<IntrospectedColumn>> columns = getColumns(tc);
if (columns.isEmpty()) {
String formattedCatalog = tc.getCatalog() == null ? "<null>" : "'" + tc.getCatalog() + "'";
String formattedSchema = tc.getSchema() == null ? "<null>" : "'" + tc.getSchema() + "'";
String formattedTableName = "'" + tc.getTableName() + "'";
warnings.add(getString("Warning.19", formattedCatalog, formattedSchema, formattedTableName)); //$NON-NLS-1$
return Collections.emptyList();
}
removeIgnoredColumns(tc, columns);
calculateExtraColumnInformation(tc, columns);
applyColumnOverrides(tc, columns);
calculateIdentityColumns(tc, columns);
List<IntrospectedTable> introspectedTables =
calculateIntrospectedTables(tc, columns, knownRuntime, pluginAggregator);
// now introspectedTables has all the columns from all the
// tables in the configuration. Do some validation...
Iterator<IntrospectedTable> iter = introspectedTables.iterator();
while (iter.hasNext()) {
IntrospectedTable introspectedTable = iter.next();
if (!introspectedTable.hasAnyColumns()) {
// add a warning that the table has no columns, remove from the
// list
String warning =
getString("Warning.1", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$
warnings.add(warning);
iter.remove();
} else if (!introspectedTable.hasPrimaryKeyColumns() && !introspectedTable.hasBaseColumns()) {
// add a warning that the table has only BLOB columns, remove from
// the list
String warning =
getString("Warning.18", introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$
warnings.add(warning);
iter.remove();
} else {
// now make sure that all columns called out in the
// configuration
// actually exist
reportIntrospectionWarnings(introspectedTable, tc, introspectedTable.getFullyQualifiedTable());
}
}
return introspectedTables;
}
private void removeIgnoredColumns(TableConfiguration tc, Map<ActualTableName, List<IntrospectedColumn>> columns) {
for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
Iterator<IntrospectedColumn> tableColumns = entry.getValue().iterator();
while (tableColumns.hasNext()) {
IntrospectedColumn introspectedColumn = tableColumns.next();
if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) {
tableColumns.remove();
if (logger.isDebugEnabled()) {
logger.debug(getString("Tracing.3", //$NON-NLS-1$
introspectedColumn.getActualColumnName(), entry.getKey().toString()));
}
}
}
}
}
private void calculateExtraColumnInformation(TableConfiguration tc, Map<ActualTableName,
List<IntrospectedColumn>> columns) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
for (IntrospectedColumn introspectedColumn : entry.getValue()) {
String calculatedColumnName = tc.getColumnRenamingRule().map(rr -> {
Matcher matcher = rr.pattern().matcher(introspectedColumn.getActualColumnName());
return matcher.replaceAll(rr.replaceString());
}).orElseGet(introspectedColumn::getActualColumnName);
if (isTrue(tc.getProperty(PropertyRegistry.TABLE_USE_ACTUAL_COLUMN_NAMES))) {
introspectedColumn.setJavaProperty(JavaBeansUtil.getValidPropertyName(calculatedColumnName));
} else if (isTrue(tc.getProperty(PropertyRegistry.TABLE_USE_COMPOUND_PROPERTY_NAMES))) {
sb.setLength(0);
sb.append(calculatedColumnName);
introspectedColumn.getRemarks().ifPresent(r -> {
sb.append('_');
sb.append(JavaBeansUtil.getCamelCaseString(r, true));
});
introspectedColumn.setJavaProperty(JavaBeansUtil.getValidPropertyName(sb.toString()));
} else {
introspectedColumn.setJavaProperty(
JavaBeansUtil.getCamelCaseString(calculatedColumnName, false));
}
javaTypeResolver.calculateTypeInformation(introspectedColumn).ifPresentOrElse(ti -> {
introspectedColumn.setFullyQualifiedJavaType(ti.fullyQualifiedJavaType());
introspectedColumn.setJdbcTypeName(ti.jdbcTypeName());
}, () -> {
// type cannot be resolved. Check for ignored or overridden
if (tc.isColumnIgnored(introspectedColumn.getActualColumnName())) {
return;
}
// if overridden, and a java type is configured, then we can ignore
if (tc.getColumnOverride(introspectedColumn.getActualColumnName())
.flatMap(ColumnOverride::getJavaType)
.isPresent()) {
return;
}
// if the type is not supported, then we'll report a warning
introspectedColumn.setFullyQualifiedJavaType(FullyQualifiedJavaType.getObjectInstance());
introspectedColumn.setJdbcTypeName("OTHER"); //$NON-NLS-1$
String warning = getString("Warning.14", //$NON-NLS-1$
Integer.toString(introspectedColumn.getJdbcType()),
entry.getKey().toString(),
introspectedColumn.getActualColumnName());
warnings.add(warning);
});
if (context.autoDelimitKeywords()
&& SqlReservedWords.containsWord(introspectedColumn.getActualColumnName())) {
introspectedColumn.setColumnNameDelimited(true);
}
if (tc.isAllColumnDelimitingEnabled()) {
introspectedColumn.setColumnNameDelimited(true);
}
}
}
}
private void calculateIdentityColumns(TableConfiguration tc,
Map<ActualTableName, List<IntrospectedColumn>> columns) {
tc.getGeneratedKey().ifPresent(gk -> columns.values().stream()
.flatMap(List::stream)
.filter(introspectedColumn -> isMatchedColumn(introspectedColumn, gk))
.forEach(introspectedColumn -> {
if (gk.isIdentity() || gk.isJdbcStandard()) {
introspectedColumn.setIdentity(true);
introspectedColumn.setSequenceColumn(false);
} else {
introspectedColumn.setIdentity(false);
introspectedColumn.setSequenceColumn(true);
}
}));
}
private boolean isMatchedColumn(IntrospectedColumn introspectedColumn, GeneratedKey gk) {
if (introspectedColumn.isColumnNameDelimited()) {
return introspectedColumn.getActualColumnName().equals(gk.getColumn());
} else {
return introspectedColumn.getActualColumnName().equalsIgnoreCase(gk.getColumn());
}
}
private void applyColumnOverrides(TableConfiguration tc, Map<ActualTableName, List<IntrospectedColumn>> columns) {
for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
for (IntrospectedColumn introspectedColumn : entry.getValue()) {
tc.getColumnOverride(introspectedColumn.getActualColumnName()).ifPresent(columnOverride -> {
if (logger.isDebugEnabled()) {
logger.debug(getString("Tracing.4", //$NON-NLS-1$
introspectedColumn.getActualColumnName(), entry.getKey().toString()));
}
columnOverride.getJavaProperty().ifPresent(introspectedColumn::setJavaProperty);
columnOverride.getJavaType().ifPresent(
jt -> introspectedColumn.setFullyQualifiedJavaType(new FullyQualifiedJavaType(jt)));
columnOverride.getJdbcType().ifPresent(introspectedColumn::setJdbcTypeName);
columnOverride.getTypeHandler().ifPresent(introspectedColumn::setTypeHandler);
if (columnOverride.isColumnNameDelimited()) {
introspectedColumn.setColumnNameDelimited(true);
}
introspectedColumn.setGeneratedAlways(columnOverride.isGeneratedAlways());
introspectedColumn.setProperties(columnOverride.getProperties());
});
}
}
}
private Map<ActualTableName, List<IntrospectedColumn>> getColumns(TableConfiguration tc) throws SQLException {
String localCatalog;
String localSchema;
String localTableName;
boolean delimitIdentifiers = tc.isDelimitIdentifiers()
|| stringContainsSpace(tc.getCatalog())
|| stringContainsSpace(tc.getSchema())
|| stringContainsSpace(tc.getTableName());
if (delimitIdentifiers) {
localCatalog = tc.getCatalog();
localSchema = tc.getSchema();
localTableName = tc.getTableName();
} else if (databaseMetaData.storesLowerCaseIdentifiers()) {
localCatalog = tc.getCatalog() == null ? null : tc.getCatalog().toLowerCase();
localSchema = tc.getSchema() == null ? null : tc.getSchema().toLowerCase();
localTableName = tc.getTableName().toLowerCase();
} else if (databaseMetaData.storesUpperCaseIdentifiers()) {
localCatalog = tc.getCatalog() == null ? null : tc.getCatalog().toUpperCase();
localSchema = tc.getSchema() == null ? null : tc.getSchema().toUpperCase();
localTableName = tc.getTableName().toUpperCase();
} else {
localCatalog = tc.getCatalog();
localSchema = tc.getSchema();
localTableName = tc.getTableName();
}
if (tc.isWildcardEscapingEnabled()) {
String escapeString = databaseMetaData.getSearchStringEscape();
if (localSchema != null) {
localSchema = escapeName(localSchema, escapeString);
}
localTableName = escapeName(localTableName, escapeString);
}
Map<ActualTableName, List<IntrospectedColumn>> answer = new HashMap<>();
if (logger.isDebugEnabled()) {
String fullTableName = composeFullyQualifiedTableName(localCatalog, localSchema,
localTableName, '.');
logger.debug(getString("Tracing.1", fullTableName)); //$NON-NLS-1$
}
try (ResultSet rs
= databaseMetaData.getColumns(localCatalog, localSchema, localTableName, "%")) { //$NON-NLS-1$
boolean supportsIsAutoIncrement = false;
boolean supportsIsGeneratedColumn = false;
ResultSetMetaData rsmd = rs.getMetaData();
int colCount = rsmd.getColumnCount();
for (int i = 1; i <= colCount; i++) {
if ("IS_AUTOINCREMENT".equals(rsmd.getColumnName(i))) { //$NON-NLS-1$
supportsIsAutoIncrement = true;
}
if ("IS_GENERATEDCOLUMN".equals(rsmd.getColumnName(i))) { //$NON-NLS-1$
supportsIsGeneratedColumn = true;
}
}
while (rs.next()) {
IntrospectedColumn introspectedColumn = ObjectFactory.createIntrospectedColumn(context);
introspectedColumn.setTableAlias(tc.getAlias());
introspectedColumn.setJdbcType(rs.getInt("DATA_TYPE")); //$NON-NLS-1$
introspectedColumn.setActualTypeName(rs.getString("TYPE_NAME")); //$NON-NLS-1$
introspectedColumn.setLength(rs.getInt("COLUMN_SIZE")); //$NON-NLS-1$
introspectedColumn.setActualColumnName(rs.getString("COLUMN_NAME")); //$NON-NLS-1$
introspectedColumn
.setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable); //$NON-NLS-1$
introspectedColumn.setScale(rs.getInt("DECIMAL_DIGITS")); //$NON-NLS-1$
introspectedColumn.setRemarks(rs.getString("REMARKS")); //$NON-NLS-1$
introspectedColumn.setDefaultValue(rs.getString("COLUMN_DEF")); //$NON-NLS-1$
if (supportsIsAutoIncrement) {
introspectedColumn.setAutoIncrement(
"YES".equals(rs.getString("IS_AUTOINCREMENT"))); //$NON-NLS-1$ //$NON-NLS-2$
}
if (supportsIsGeneratedColumn) {
introspectedColumn.setGeneratedColumn(
"YES".equals(rs.getString("IS_GENERATEDCOLUMN"))); //$NON-NLS-1$ //$NON-NLS-2$
}
ActualTableName atn = new ActualTableName(
rs.getString("TABLE_CAT"), //$NON-NLS-1$
rs.getString("TABLE_SCHEM"), //$NON-NLS-1$
rs.getString("TABLE_NAME")); //$NON-NLS-1$
List<IntrospectedColumn> columns = answer.computeIfAbsent(atn, k -> new ArrayList<>());
columns.add(introspectedColumn);
if (logger.isDebugEnabled()) {
logger.debug(getString(
"Tracing.2", //$NON-NLS-1$
introspectedColumn.getActualColumnName(),
Integer.toString(introspectedColumn.getJdbcType()),
atn.toString()));
}
}
}
if (answer.size() > 1
&& !stringContainsSQLWildcard(localSchema)
&& !stringContainsSQLWildcard(localTableName)) {
// issue a warning if there is more than one table and
// no wildcards were used
ActualTableName inputAtn = new ActualTableName(tc.getCatalog(), tc.getSchema(), tc.getTableName());
StringBuilder sb = new StringBuilder();
boolean comma = false;
for (ActualTableName atn : answer.keySet()) {
if (comma) {
sb.append(',');
} else {
comma = true;
}
sb.append(atn);
}
warnings.add(getString("Warning.25", inputAtn.toString(), sb.toString())); //$NON-NLS-1$
}
return answer;
}
private String escapeName(String localName, String escapeString) {
StringTokenizer st = new StringTokenizer(localName, "_%", true); //$NON-NLS-1$
StringBuilder sb = new StringBuilder();
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (token.equals("_") || token.equals("%")) { //$NON-NLS-1$ //$NON-NLS-2$
sb.append(escapeString);
}
sb.append(token);
}
return sb.toString();
}
private List<IntrospectedTable> calculateIntrospectedTables(TableConfiguration tc, Map<ActualTableName,
List<IntrospectedColumn>> columns, KnownRuntime knownRuntime, PluginAggregator pluginAggregator) {
boolean delimitIdentifiers = tc.isDelimitIdentifiers()
|| stringContainsSpace(tc.getCatalog())
|| stringContainsSpace(tc.getSchema())
|| stringContainsSpace(tc.getTableName());
List<IntrospectedTable> answer = new ArrayList<>();
for (Map.Entry<ActualTableName, List<IntrospectedColumn>> entry : columns.entrySet()) {
ActualTableName atn = entry.getKey();
// we only use the returned catalog and schema if something was
// actually
// specified on the table configuration. If something was returned
// from the DB for these fields, but nothing was specified on the
// table
// configuration, then some sort of DB default is being returned,
// and we don't want that in our SQL
FullyQualifiedTable table = new FullyQualifiedTable.Builder()
.withIntrospectedCatalog(stringHasValue(tc.getCatalog()) ? atn.getCatalog() : null)
.withIntrospectedSchema(stringHasValue(tc.getSchema()) ? atn.getSchema() : null)
.withIntrospectedTableName(atn.getTableName())
.withDomainObjectName(tc.getDomainObjectName())
.withAlias(tc.getAlias())
.withIgnoreQualifiersAtRuntime(
isTrue(tc.getProperty(PropertyRegistry.TABLE_IGNORE_QUALIFIERS_AT_RUNTIME)))
.withRuntimeCatalog(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_CATALOG))
.withRuntimeSchema(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_SCHEMA))
.withRuntimeTableName(tc.getProperty(PropertyRegistry.TABLE_RUNTIME_TABLE_NAME))
.withDelimitIdentifiers(delimitIdentifiers)
.withDomainObjectRenamingRule(tc.getDomainObjectRenamingRule())
.withContext(context)
.build();
IntrospectedTable introspectedTable = new IntrospectedTable.Builder()
.withTableConfiguration(tc)
.withFullyQualifiedTable(table)
.withContext(context)
.withKnownRuntime(knownRuntime)
.withPluginAggregator(pluginAggregator)
.build();
for (IntrospectedColumn introspectedColumn : entry.getValue()) {
introspectedTable.addColumn(introspectedColumn);
}
calculatePrimaryKey(table, introspectedTable);
enhanceIntrospectedTable(introspectedTable);
answer.add(introspectedTable);
}
return answer;
}
/**
* Calls database metadata to retrieve extra information about the table
* such as remarks associated with the table and the type.
*
* <p>If there is any error, we just add a warning and continue.
*
* @param introspectedTable the introspected table to enhance
*/
private void enhanceIntrospectedTable(IntrospectedTable introspectedTable) {
FullyQualifiedTable fqt = introspectedTable.getFullyQualifiedTable();
try (ResultSet rs = databaseMetaData.getTables(fqt.getIntrospectedCatalog().orElse(null),
fqt.getIntrospectedSchema().orElse(null),
fqt.getIntrospectedTableName(), null)) {
if (rs.next()) {
String remarks = rs.getString("REMARKS"); //$NON-NLS-1$
String tableType = rs.getString("TABLE_TYPE"); //$NON-NLS-1$
introspectedTable.setRemarks(remarks);
introspectedTable.setTableType(tableType);
}
} catch (SQLException e) {
warnings.add(getString("Warning.27", e.getMessage())); //$NON-NLS-1$
}
}
}