/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* 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
*
* 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.jkiss.dbeaver.ext.generic.model;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ext.generic.GenericConstants;
import org.jkiss.dbeaver.ext.generic.model.meta.GenericMetaModel;
import org.jkiss.dbeaver.ext.generic.model.meta.GenericMetaObject;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.exec.DBCSession;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCDatabaseMetaData;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession;
import org.jkiss.dbeaver.model.impl.AbstractExecutionSource;
import org.jkiss.dbeaver.model.impl.jdbc.JDBCConstants;
import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils;
import org.jkiss.dbeaver.model.impl.jdbc.struct.JDBCTable;
import org.jkiss.dbeaver.model.meta.Association;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.struct.DBSEntityConstraintType;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.model.struct.rdb.DBSForeignKeyDefferability;
import org.jkiss.dbeaver.model.struct.rdb.DBSForeignKeyModifyRule;
import org.jkiss.utils.CommonUtils;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.*;
/**
* Generic table
*/
public class GenericTable extends JDBCTable<GenericDataSource, GenericStructContainer> implements DBPRefreshableObject, DBPSystemObject, DBPScriptObject
{
private static final Log log = Log.getLog(GenericTable.class);
private String tableType;
private boolean isView;
private boolean isSystem;
private String description;
private Long rowCount;
private List<? extends GenericTrigger> triggers;
private String ddl;
public GenericTable(
GenericStructContainer container,
@Nullable String tableName,
@Nullable String tableType,
@Nullable JDBCResultSet dbResult)
{
super(container, tableName, dbResult != null);
this.tableType = tableType;
if (this.tableType == null) {
this.tableType = "";
}
if (dbResult != null) {
this.description = GenericUtils.safeGetString(container.getTableCache().tableObject, dbResult, JDBCConstants.REMARKS);
}
final GenericMetaModel metaModel = container.getDataSource().getMetaModel();
this.isView = metaModel.isView(this);
this.isSystem = metaModel.isSystemTable(this);
}
@Override
public TableCache getCache()
{
return getContainer().getTableCache();
}
@Override
public DBSObject getParentObject()
{
return getContainer().getObject();
}
@NotNull
@Override
public String getFullyQualifiedName(DBPEvaluationContext context)
{
if (isView() && context == DBPEvaluationContext.DDL && !getDataSource().getMetaModel().useCatalogInObjectNames()) {
// [SQL Server] workaround. You can't use catalog name in operations with views.
return DBUtils.getFullQualifiedName(
getDataSource(),
getSchema(),
this);
}
return DBUtils.getFullQualifiedName(
getDataSource(),
getCatalog(),
getSchema(),
this);
}
@Override
public boolean isView()
{
return this.isView;
}
@Override
public boolean isSystem()
{
return this.isSystem;
}
@Property(viewable = true, order = 2)
public String getTableType()
{
return tableType;
}
@Property(viewable = true, order = 3)
public GenericCatalog getCatalog()
{
return getContainer().getCatalog();
}
@Property(viewable = true, order = 4)
public GenericSchema getSchema()
{
return getContainer().getSchema();
}
@Nullable
@Override
public synchronized Collection<GenericTableColumn> getAttributes(@NotNull DBRProgressMonitor monitor)
throws DBException
{
return this.getContainer().getTableCache().getChildren(monitor, getContainer(), this);
}
@Override
public GenericTableColumn getAttribute(@NotNull DBRProgressMonitor monitor, @NotNull String attributeName)
throws DBException
{
return this.getContainer().getTableCache().getChild(monitor, getContainer(), this, attributeName);
}
@Override
public synchronized Collection<GenericTableIndex> getIndexes(DBRProgressMonitor monitor)
throws DBException
{
// Read indexes using cache
return this.getContainer().getIndexCache().getObjects(monitor, getContainer(), this);
}
@Nullable
@Override
public synchronized Collection<GenericPrimaryKey> getConstraints(@NotNull DBRProgressMonitor monitor)
throws DBException
{
// ensure all columns are already cached
getAttributes(monitor);
return getContainer().getPrimaryKeysCache().getObjects(monitor, getContainer(), this);
}
synchronized void addUniqueKey(GenericPrimaryKey constraint) {
getContainer().getPrimaryKeysCache().cacheObject(constraint);
}
@Override
public Collection<GenericTableForeignKey> getReferences(@NotNull DBRProgressMonitor monitor)
throws DBException
{
return loadReferences(monitor);
}
@Override
public synchronized Collection<GenericTableForeignKey> getAssociations(@NotNull DBRProgressMonitor monitor)
throws DBException
{
if (getDataSource().getInfo().supportsReferentialIntegrity()) {
return getContainer().getForeignKeysCache().getObjects(monitor, getContainer(), this);
}
return null;
}
@Nullable
public Collection<GenericTable> getSubTables()
{
return null;
}
@Nullable
@Override
@Property(viewable = true, order = 100)
public String getDescription()
{
return description;
}
@Override
public synchronized DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBException
{
this.getContainer().getIndexCache().clearObjectCache(this);
this.getContainer().getPrimaryKeysCache().clearObjectCache(this);
this.getContainer().getForeignKeysCache().clearObjectCache(this);
return this.getContainer().getTableCache().refreshObject(monitor, getContainer(), this);
}
// Comment row count calculation - it works too long and takes a lot of resources without serious reason
@Nullable
@Property(viewable = true, expensive = true, order = 5, category = "Statistics")
public synchronized Long getRowCount(DBRProgressMonitor monitor)
{
if (rowCount != null) {
return rowCount;
}
if (isView() || !isPersisted()) {
// Do not count rows for views
return null;
}
if (Boolean.FALSE.equals(getDataSource().getContainer().getDriver().getDriverParameter(GenericConstants.PARAM_SUPPORTS_SELECT_COUNT))) {
// Select count not supported
return null;
}
if (rowCount == null) {
// Query row count
try (DBCSession session = DBUtils.openUtilSession(monitor, getDataSource(), "Read row count")) {
rowCount = countData(
new AbstractExecutionSource(this, session.getExecutionContext(), this), session, null);
} catch (DBException e) {
// do not throw this error - row count is optional info and some providers may fail
log.debug("Can't fetch row count: " + e.getMessage());
// if (indexes != null) {
// rowCount = getRowCountFromIndexes(monitor);
// }
}
}
if (rowCount == null) {
rowCount = -1L;
}
return rowCount;
}
@Nullable
public Long getRowCountFromIndexes(DBRProgressMonitor monitor)
{
try {
// Try to get cardinality from some unique index
// Cardinality
final Collection<GenericTableIndex> indexList = getIndexes(monitor);
if (!CommonUtils.isEmpty(indexList)) {
for (GenericTableIndex index : indexList) {
if (index.isUnique()/* || index.getIndexType() == DBSIndexType.STATISTIC*/) {
final long cardinality = index.getCardinality();
if (cardinality > 0) {
return cardinality;
}
}
}
}
} catch (DBException e) {
log.error(e);
}
return null;
}
@Override
public String getObjectDefinitionText(DBRProgressMonitor monitor) throws DBException {
if (ddl == null) {
if (isView()) {
ddl = getDataSource().getMetaModel().getViewDDL(monitor, this);
} else if (!isPersisted()) {
ddl = "";
} else {
ddl = getDataSource().getMetaModel().getTableDDL(monitor, this);
}
}
return ddl;
}
private static class ForeignKeyInfo {
String pkColumnName;
String fkTableCatalog;
String fkTableSchema;
String fkTableName;
String fkColumnName;
int keySeq;
int updateRuleNum;
int deleteRuleNum;
String fkName;
String pkName;
int defferabilityNum;
}
private synchronized List<GenericTableForeignKey> loadReferences(DBRProgressMonitor monitor)
throws DBException
{
if (!isPersisted() || !getDataSource().getInfo().supportsReferentialIntegrity()) {
return new ArrayList<>();
}
try (JDBCSession session = DBUtils.openMetaSession(monitor, getDataSource(), "Load table relations")) {
// Read foreign keys in two passes
// First read entire resultset to prevent recursive metadata requests
// some drivers don't like it
final GenericMetaObject fkObject = getDataSource().getMetaObject(GenericConstants.OBJECT_FOREIGN_KEY);
final List<ForeignKeyInfo> fkInfos = new ArrayList<>();
JDBCDatabaseMetaData metaData = session.getMetaData();
// Load indexes
try (JDBCResultSet dbResult = metaData.getExportedKeys(
getCatalog() == null ? null : getCatalog().getName(),
getSchema() == null ? null : getSchema().getName(),
getName())) {
while (dbResult.next()) {
ForeignKeyInfo fkInfo = new ForeignKeyInfo();
fkInfo.pkColumnName = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.PKCOLUMN_NAME);
fkInfo.fkTableCatalog = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.FKTABLE_CAT);
fkInfo.fkTableSchema = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.FKTABLE_SCHEM);
fkInfo.fkTableName = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.FKTABLE_NAME);
fkInfo.fkColumnName = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.FKCOLUMN_NAME);
fkInfo.keySeq = GenericUtils.safeGetInt(fkObject, dbResult, JDBCConstants.KEY_SEQ);
fkInfo.updateRuleNum = GenericUtils.safeGetInt(fkObject, dbResult, JDBCConstants.UPDATE_RULE);
fkInfo.deleteRuleNum = GenericUtils.safeGetInt(fkObject, dbResult, JDBCConstants.DELETE_RULE);
fkInfo.fkName = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.FK_NAME);
fkInfo.pkName = GenericUtils.safeGetStringTrimmed(fkObject, dbResult, JDBCConstants.PK_NAME);
fkInfo.defferabilityNum = GenericUtils.safeGetInt(fkObject, dbResult, JDBCConstants.DEFERRABILITY);
fkInfos.add(fkInfo);
}
}
List<GenericTableForeignKey> fkList = new ArrayList<>();
Map<String, GenericTableForeignKey> fkMap = new HashMap<>();
for (ForeignKeyInfo info : fkInfos) {
DBSForeignKeyModifyRule deleteRule = JDBCUtils.getCascadeFromNum(info.deleteRuleNum);
DBSForeignKeyModifyRule updateRule = JDBCUtils.getCascadeFromNum(info.updateRuleNum);
DBSForeignKeyDefferability defferability;
switch (info.defferabilityNum) {
case DatabaseMetaData.importedKeyInitiallyDeferred: defferability = DBSForeignKeyDefferability.INITIALLY_DEFERRED; break;
case DatabaseMetaData.importedKeyInitiallyImmediate: defferability = DBSForeignKeyDefferability.INITIALLY_IMMEDIATE; break;
case DatabaseMetaData.importedKeyNotDeferrable: defferability = DBSForeignKeyDefferability.NOT_DEFERRABLE; break;
default: defferability = DBSForeignKeyDefferability.UNKNOWN; break;
}
if (info.fkTableName == null) {
log.debug("Null FK table name");
continue;
}
//String fkTableFullName = DBUtils.getFullyQualifiedName(getDataSource(), info.fkTableCatalog, info.fkTableSchema, info.fkTableName);
GenericTable fkTable = getDataSource().findTable(monitor, info.fkTableCatalog, info.fkTableSchema, info.fkTableName);
if (fkTable == null) {
log.warn("Can't find FK table " + info.fkTableName);
continue;
}
GenericTableColumn pkColumn = this.getAttribute(monitor, info.pkColumnName);
if (pkColumn == null) {
log.warn("Can't find PK column " + info.pkColumnName);
continue;
}
GenericTableColumn fkColumn = fkTable.getAttribute(monitor, info.fkColumnName);
if (fkColumn == null) {
log.warn("Can't find FK table " + fkTable.getFullyQualifiedName(DBPEvaluationContext.DDL) + " column " + info.fkColumnName);
continue;
}
// Find PK
GenericPrimaryKey pk = null;
if (!CommonUtils.isEmpty(info.pkName)) {
pk = DBUtils.findObject(this.getConstraints(monitor), info.pkName);
if (pk == null) {
log.debug("Unique key '" + info.pkName + "' not found in table " + this.getFullyQualifiedName(DBPEvaluationContext.DDL));
}
}
if (pk == null) {
Collection<GenericPrimaryKey> uniqueKeys = this.getConstraints(monitor);
if (uniqueKeys != null) {
for (GenericPrimaryKey pkConstraint : uniqueKeys) {
if (pkConstraint.getConstraintType().isUnique() && DBUtils.getConstraintAttribute(monitor, pkConstraint, pkColumn) != null) {
pk = pkConstraint;
break;
}
}
}
}
if (pk == null) {
log.warn("Can't find unique key for table " + this.getFullyQualifiedName(DBPEvaluationContext.DDL) + " column " + pkColumn.getName());
// Too bad. But we have to create new fake PK for this FK
//String pkFullName = getFullyQualifiedName() + "." + info.pkName;
pk = new GenericPrimaryKey(this, info.pkName, null, DBSEntityConstraintType.PRIMARY_KEY, true);
pk.addColumn(new GenericTableConstraintColumn(pk, pkColumn, info.keySeq));
// Add this fake constraint to it's owner
this.addUniqueKey(pk);
}
// Find (or create) FK
GenericTableForeignKey fk;
if (CommonUtils.isEmpty(info.fkName)) {
// Make fake FK name
info.fkName = info.fkTableName.toUpperCase() + "_FK" + info.keySeq;
fk = DBUtils.findObject(fkTable.getAssociations(monitor), info.fkName);
} else {
fk = DBUtils.findObject(fkTable.getAssociations(monitor), info.fkName);
if (fk == null) {
log.warn("Can't find foreign key '" + info.fkName + "' for table " + fkTable.getFullyQualifiedName(DBPEvaluationContext.DDL));
// No choice, we have to create fake foreign key :(
}
}
if (fk != null && !fkList.contains(fk)) {
fkList.add(fk);
}
if (fk == null) {
fk = fkMap.get(info.fkName);
if (fk == null) {
fk = new GenericTableForeignKey(fkTable, info.fkName, null, pk, deleteRule, updateRule, defferability, true);
fkMap.put(info.fkName, fk);
fkList.add(fk);
}
GenericTableForeignKeyColumnTable fkColumnInfo = new GenericTableForeignKeyColumnTable(fk, fkColumn, info.keySeq, pkColumn);
fk.addColumn(fkColumnInfo);
}
}
return fkList;
} catch (SQLException ex) {
throw new DBException(ex, getDataSource());
}
}
@Association
public Collection<? extends GenericTrigger> getTriggers(DBRProgressMonitor monitor) throws DBException {
if (triggers == null) {
loadTriggers(monitor);
}
return triggers;
}
private void loadTriggers(DBRProgressMonitor monitor) throws DBException {
triggers = getDataSource().getMetaModel().loadTriggers(monitor, getContainer(), this);
if (triggers == null) {
triggers = new ArrayList<>();
} else {
DBUtils.orderObjects(triggers);
}
}
}