/*
* Copyright 2014-2015 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
*
* 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.
*/
// Created on 2014年12月25日
// $Id$
package com.wplatform.ddal.dbobject.table;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.sql.DataSource;
import com.wplatform.ddal.command.ddl.CreateTableData;
import com.wplatform.ddal.dbobject.index.Index;
import com.wplatform.ddal.dbobject.index.IndexMate;
import com.wplatform.ddal.dbobject.index.IndexType;
import com.wplatform.ddal.dbobject.schema.Schema;
import com.wplatform.ddal.dispatch.rule.RoutingResult;
import com.wplatform.ddal.dispatch.rule.RuleColumn;
import com.wplatform.ddal.dispatch.rule.TableNode;
import com.wplatform.ddal.dispatch.rule.TableRouter;
import com.wplatform.ddal.engine.Session;
import com.wplatform.ddal.excutor.Optional;
import com.wplatform.ddal.message.DbException;
import com.wplatform.ddal.message.ErrorCode;
import com.wplatform.ddal.shards.DataSourceRepository;
import com.wplatform.ddal.util.JdbcUtils;
import com.wplatform.ddal.util.MathUtils;
import com.wplatform.ddal.util.New;
import com.wplatform.ddal.util.StringUtils;
import com.wplatform.ddal.value.DataType;
import com.wplatform.ddal.value.ValueDate;
import com.wplatform.ddal.value.ValueTime;
import com.wplatform.ddal.value.ValueTimestamp;
/**
* @author <a href="mailto:jorgie.mail@gmail.com">jorgie li</a>
*/
public class TableMate extends Table {
private static final int MAX_RETRY = 2;
private static final long ROW_COUNT_APPROXIMATION = 100000;
private final boolean globalTemporary;
private final ArrayList<Index> indexes = New.arrayList();
private Index scanIndex;
private TableRouter tableRouter;
private TableNode[] shards;
private int scanLevel;
private DbException initException;
private boolean storesLowerCase;
private boolean storesMixedCase;
private boolean storesMixedCaseQuoted;
private boolean supportsMixedCaseIdentifiers;
public TableMate(CreateTableData data) {
super(data.schema, data.id, data.tableName);
this.globalTemporary = data.globalTemporary;
setTemporary(data.temporary);
Column[] cols = new Column[data.columns.size()];
data.columns.toArray(cols);
setColumns(cols);
scanIndex = new IndexMate(this, data.id, null, IndexColumn.wrap(cols), IndexType.createScan());
indexes.add(scanIndex);
}
public TableMate(Schema schema, int id, String name) {
super(schema, id, name);
this.globalTemporary = false;
}
/**
* @return the tableRouter
*/
public TableRouter getTableRouter() {
return tableRouter;
}
/**
* @param tableRouter the tableRouter to set
*/
public void setTableRouter(TableRouter tableRouter) {
this.tableRouter = tableRouter;
}
/**
* @return the scanLevel
*/
public int getScanLevel() {
return scanLevel;
}
/**
* @param scanLevel the scanLevel to set
*/
public void setScanLevel(int scanLevel) {
this.scanLevel = scanLevel;
}
/**
* @return the shards
*/
public TableNode[] getShards() {
return shards;
}
/**
* test the table is global table.
* @return
*/
public boolean isReplication() {
return shards != null && shards.length > 1;
}
/**
* @param shards the shards to set
*/
public void setShards(TableNode[] shards) {
this.shards = shards;
}
/**
* @return
* @see com.wplatform.ddal.dispatch.rule.TableRouter#getPartition()
*/
public TableNode[] getPartitionNode() {
if (tableRouter != null) {
List<TableNode> partition = tableRouter.getPartition();
return partition.toArray(new TableNode[partition.size()]);
}
return shards;
}
/**
* validation the rule columns is in the table columns
*/
private void validationRuleColumn(Column[] columns) {
if (tableRouter != null) {
for (RuleColumn ruleCol : tableRouter.getRuleColumns()) {
Column matched = null;
for (Column column : columns) {
String colName = column.getName();
if (colName.equalsIgnoreCase(ruleCol.getName())) {
matched = column;
break;
}
}
if (matched == null) {
throw DbException.throwInternalError(
"The rule column " + ruleCol + " does not exist in " + getName() + " table.");
}
}
}
}
public void check() {
if (initException != null) {
Column[] cols = {};
setColumns(cols);
indexes.clear();
throw initException;
}
}
public boolean isInited() {
return initException != null;
}
public void markDeleted() {
Column[] cols = {};
setColumns(cols);
indexes.clear();
initException = DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, this.getSQL());
}
@Override
public boolean isGlobalTemporary() {
return globalTemporary;
}
@Override
public String getTableType() {
return Table.TABLE;
}
@Override
public Index getUniqueIndex() {
for (Index idx : indexes) {
if (idx.getIndexType().isUnique()) {
return idx;
}
}
return null;
}
@Override
public ArrayList<Index> getIndexes() {
return indexes;
}
@Override
public boolean isDeterministic() {
return false;
}
@Override
public boolean canGetRowCount() {
return false;
}
@Override
public long getRowCount(Session session) {
return 0;
}
@Override
public long getRowCountApproximation() {
if (this.tableRouter == null) {
return ROW_COUNT_APPROXIMATION;
} else {
return tableRouter.getPartition().size() * ROW_COUNT_APPROXIMATION;
}
}
@Override
public void checkRename() {
}
@Override
public Index getScanIndex(Session session) {
return scanIndex;
}
private Index addIndex(ArrayList<Column> list, IndexType indexType) {
Column[] cols = new Column[list.size()];
list.toArray(cols);
Index index = new IndexMate(this, 0, null, IndexColumn.wrap(cols), indexType);
indexes.add(index);
return index;
}
public void loadMataData(Session session) {
TableNode[] nodes = getPartitionNode();
if (nodes == null || nodes.length < 1) {
throw new IllegalStateException();
}
TableNode matadataNode = nodes[0];
String tableName = matadataNode.getCompositeObjectName();
String shardName = matadataNode.getShardName();
try {
trace.debug("Try to load {0} metadata from table {1}.{2}", getName(), shardName, tableName);
readMataData(session, matadataNode);
trace.debug("Load the {0} metadata success.", getName());
initException = null;
} catch (DbException e) {
trace.debug("Fail to load {0} metadata from table {1}.{2}. error: {3}", getName(), shardName, tableName,
e.getCause().getMessage());
initException = e;
Column[] cols = {};
setColumns(cols);
scanIndex = new IndexMate(this, 0, null, IndexColumn.wrap(cols), IndexType.createNonUnique());
indexes.add(scanIndex);
}
}
/**
* @param session
*/
public void readMataData(Session session, TableNode matadataNode) {
for (int retry = 0;; retry++) {
try {
Connection conn = null;
String tableName = matadataNode.getCompositeObjectName();
String shardName = matadataNode.getShardName();
try {
DataSourceRepository dsRepository = session.getDataSourceRepository();
DataSource dataSource = dsRepository.getDataSourceByShardName(shardName);
Optional optional = Optional.build().shardName(shardName).readOnly(false);
conn = session.applyConnection(dataSource, optional);
tableName = database.identifier(tableName);
tryReadMetaData(conn, tableName);
return;
} catch (Exception e) {
throw DbException.convert(e);
} finally {
JdbcUtils.closeSilently(conn);
}
} catch (DbException e) {
if (retry >= MAX_RETRY) {
throw e;
}
}
}
}
private void tryReadMetaData(Connection conn, String tableName) throws SQLException {
DatabaseMetaData meta = conn.getMetaData();
storesLowerCase = meta.storesLowerCaseIdentifiers();
storesMixedCase = meta.storesMixedCaseIdentifiers();
storesMixedCaseQuoted = meta.storesMixedCaseQuotedIdentifiers();
supportsMixedCaseIdentifiers = meta.supportsMixedCaseIdentifiers();
ResultSet rs = meta.getTables(null, null, tableName, null);
if (rs.next() && rs.next()) {
throw DbException.get(ErrorCode.SCHEMA_NAME_MUST_MATCH, tableName);
}
rs.close();
rs = meta.getColumns(null, null, tableName, null);
int i = 0;
ArrayList<Column> columnList = New.arrayList();
HashMap<String, Column> columnMap = New.hashMap();
String catalog = null, schema = null;
while (rs.next()) {
String thisCatalog = rs.getString("TABLE_CAT");
if (catalog == null) {
catalog = thisCatalog;
}
String thisSchema = rs.getString("TABLE_SCHEM");
if (schema == null) {
schema = thisSchema;
}
if (!StringUtils.equals(catalog, thisCatalog) || !StringUtils.equals(schema, thisSchema)) {
// if the table exists in multiple schemas or tables,
// use the alternative solution
columnMap.clear();
columnList.clear();
break;
}
String n = rs.getString("COLUMN_NAME");
n = convertColumnName(n);
int sqlType = rs.getInt("DATA_TYPE");
long precision = rs.getInt("COLUMN_SIZE");
precision = convertPrecision(sqlType, precision);
int scale = rs.getInt("DECIMAL_DIGITS");
scale = convertScale(sqlType, scale);
int displaySize = MathUtils.convertLongToInt(precision);
int type = DataType.convertSQLTypeToValueType(sqlType);
Column col = new Column(n, type, precision, scale, displaySize);
col.setTable(this, i++);
columnList.add(col);
columnMap.put(n, col);
}
rs.close();
// check if the table is accessible
Statement stat = null;
try {
stat = conn.createStatement();
rs = stat.executeQuery("SELECT * FROM " + tableName + " T WHERE 1=0");
if (columnList.size() == 0) {
// alternative solution
ResultSetMetaData rsMeta = rs.getMetaData();
for (i = 0; i < rsMeta.getColumnCount();) {
String n = rsMeta.getColumnName(i + 1);
n = convertColumnName(n);
int sqlType = rsMeta.getColumnType(i + 1);
long precision = rsMeta.getPrecision(i + 1);
precision = convertPrecision(sqlType, precision);
int scale = rsMeta.getScale(i + 1);
scale = convertScale(sqlType, scale);
int displaySize = rsMeta.getColumnDisplaySize(i + 1);
int type = DataType.getValueTypeFromResultSet(rsMeta, i + 1);
Column col = new Column(n, type, precision, scale, displaySize);
col.setTable(this, i++);
columnList.add(col);
columnMap.put(n, col);
}
}
rs.close();
} catch (Exception e) {
throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, e, tableName + "(" + e.toString() + ")");
} finally {
JdbcUtils.closeSilently(stat);
}
Column[] cols = new Column[columnList.size()];
columnList.toArray(cols);
validationRuleColumn(cols);
setColumns(cols);
// create scan index
int id = getId();
scanIndex = new IndexMate(this, id, null, IndexColumn.wrap(cols), IndexType.createNonUnique());
indexes.add(scanIndex);
// create shardingKey index
if (tableRouter != null) {
ArrayList<Column> shardCol = New.arrayList();
for (RuleColumn ruleCol : tableRouter.getRuleColumns()) {
for (Column column : columns) {
String colName = column.getName();
if (colName.equalsIgnoreCase(ruleCol.getName())) {
shardCol.add(column);
}
}
}
addIndex(shardCol, IndexType.createShardingKey(false));
}
// load primary keys
try {
rs = meta.getPrimaryKeys(null, null, tableName);
} catch (Exception e) {
// Some ODBC bridge drivers don't support it:
// some combinations of "DataDirect SequeLink(R) for JDBC"
// http://www.datadirect.com/index.ssp
rs = null;
}
String pkName = "";
ArrayList<Column> list;
if (rs != null && rs.next()) {
// the problem is, the rows are not sorted by KEY_SEQ
list = New.arrayList();
do {
int idx = rs.getInt("KEY_SEQ");
if (pkName == null) {
pkName = rs.getString("PK_NAME");
}
while (list.size() < idx) {
list.add(null);
}
String col = rs.getString("COLUMN_NAME");
col = convertColumnName(col);
Column column = columnMap.get(col);
if (idx == 0) {
// workaround for a bug in the SQLite JDBC driver
list.add(column);
} else {
list.set(idx - 1, column);
}
} while (rs.next());
addIndex(list, IndexType.createPrimaryKey(false));
rs.close();
}
try {
rs = meta.getIndexInfo(null, null, tableName, false, true);
} catch (Exception e) {
// Oracle throws an exception if the table is not found or is a
// SYNONYM
rs = null;
}
String indexName = null;
list = New.arrayList();
IndexType indexType = null;
if (rs != null) {
while (rs.next()) {
if (rs.getShort("TYPE") == DatabaseMetaData.tableIndexStatistic) {
// ignore index statistics
continue;
}
String newIndex = rs.getString("INDEX_NAME");
if (pkName.equals(newIndex)) {
continue;
}
if (indexName != null && !indexName.equals(newIndex)) {
addIndex(list, indexType);
indexName = null;
}
if (indexName == null) {
indexName = newIndex;
list.clear();
}
boolean unique = !rs.getBoolean("NON_UNIQUE");
indexType = unique ? IndexType.createUnique(false) : IndexType.createNonUnique();
String col = rs.getString("COLUMN_NAME");
col = convertColumnName(col);
Column column = columnMap.get(col);
list.add(column);
}
rs.close();
}
if (indexName != null) {
addIndex(list, indexType);
}
}
private String convertColumnName(String columnName) {
if ((storesMixedCase || storesLowerCase) && columnName.equals(StringUtils.toLowerEnglish(columnName))) {
columnName = StringUtils.toUpperEnglish(columnName);
} else if (storesMixedCase && !supportsMixedCaseIdentifiers) {
// TeraData
columnName = StringUtils.toUpperEnglish(columnName);
} else if (storesMixedCase && storesMixedCaseQuoted) {
// MS SQL Server (identifiers are case insensitive even if quoted)
columnName = StringUtils.toUpperEnglish(columnName);
}
return columnName;
}
private static long convertPrecision(int sqlType, long precision) {
// workaround for an Oracle problem:
// for DATE columns, the reported precision is 7
// for DECIMAL columns, the reported precision is 0
switch (sqlType) {
case Types.DECIMAL:
case Types.NUMERIC:
if (precision == 0) {
precision = 65535;
}
break;
case Types.DATE:
precision = Math.max(ValueDate.PRECISION, precision);
break;
case Types.TIMESTAMP:
precision = Math.max(ValueTimestamp.PRECISION, precision);
break;
case Types.TIME:
precision = Math.max(ValueTime.PRECISION, precision);
break;
}
return precision;
}
private static int convertScale(int sqlType, int scale) {
// workaround for an Oracle problem:
// for DECIMAL columns, the reported precision is -127
switch (sqlType) {
case Types.DECIMAL:
case Types.NUMERIC:
if (scale < 0) {
scale = 32767;
}
break;
}
return scale;
}
public boolean isRelationSymmetry(TableMate o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
if(tableRouter != null) {
if(o.tableRouter != null) {
return tableRouter.equals(o.tableRouter);
} else {
return isNodeMatch(getPartitionNode(), o.getPartitionNode());
}
} else {
return isNodeMatch(getPartitionNode(), o.getPartitionNode());
}
}
private static boolean isNodeMatch(TableNode[] nodes1, TableNode[] nodes2) {
TableNode[] group1 = RoutingResult.fixedResult(nodes1).group();
TableNode[] group2 = RoutingResult.fixedResult(nodes2).group();
for (TableNode tableNode1 : group1) {
boolean isMatched = false;
for (TableNode tableNode2 : group2) {
if (tableNode1.getShardName().equals(tableNode2.getShardName())) {
isMatched = true;
break;
}
}
if (!isMatched) {
return false;
}
}
return true;
}
}