package com.eas.client; import com.eas.client.metadata.DbTableIndexes; import com.eas.client.metadata.DbTableKeys; import com.eas.client.dataflow.ColumnsIndicies; import com.eas.client.dataflow.StatementsGenerator; import com.eas.client.metadata.DbTableIndexColumnSpec; import com.eas.client.metadata.DbTableIndexSpec; import com.eas.client.metadata.Field; import com.eas.client.metadata.Fields; import com.eas.client.metadata.ForeignKeySpec; import com.eas.client.metadata.JdbcField; import com.eas.client.metadata.PrimaryKeySpec; import com.eas.client.sqldrivers.SqlDriver; import com.eas.concurrent.CallableConsumer; import com.eas.util.IdGenerator; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.sql.DataSource; /** * * @author mg */ public class MetadataCache implements StatementsGenerator.TablesContainer { protected class CaseInsesitiveMap<V> extends HashMap<String, V> { protected String keyToLowerCase(String aKey) { return aKey != null ? aKey.toLowerCase() : null; } @Override public V get(Object key) { return super.get(keyToLowerCase((String) key)); } @Override public V put(String key, V value) { return super.put(keyToLowerCase(key), value); } @Override public V remove(Object key) { return super.remove(keyToLowerCase((String) key)); } @Override public boolean containsKey(Object key) { return super.containsKey(keyToLowerCase((String) key)); } } protected class ConcurrentCaseInsesitiveMap<V> extends ConcurrentHashMap<String, V> { protected final String nullKey = "null-" + IdGenerator.genId(); protected String keyToLowerCase(String aKey) { return aKey != null ? aKey.toLowerCase() : nullKey; } @Override public V get(Object key) { return super.get(keyToLowerCase((String) key)); } @Override public V put(String key, V value) { return super.put(keyToLowerCase(key), value); } @Override public V remove(Object key) { return super.remove(keyToLowerCase((String) key)); } @Override public boolean containsKey(Object key) { return super.containsKey(keyToLowerCase((String) key)); } } protected String datasourceName; protected DatabasesClient client; // Named tables fields cache protected Map<String, TablesFieldsCache> schemasTablesFields = new ConcurrentCaseInsesitiveMap<>(); // Named tables indexes cache protected Map<String, TablesIndexesCache> schemasTablesIndexes = new ConcurrentCaseInsesitiveMap<>(); protected String datasourceSchema; protected SqlDriver datasourceDriver; public MetadataCache(DatabasesClient aClient, String aDatasourceName) throws Exception { super(); client = aClient; datasourceName = aDatasourceName; } public String getDatasourceSchema() throws Exception { if (datasourceSchema == null) { datasourceSchema = client.getConnectionSchema(datasourceName); } return datasourceSchema; } public SqlDriver getDatasourceSqlDriver() throws Exception { if (datasourceDriver == null) { datasourceDriver = client.getConnectionDriver(datasourceName); } return datasourceDriver; } private String schemaFromTableName(String aTableName) { int indexOfDot = aTableName.indexOf("."); String schema = null; if (indexOfDot != -1) { schema = aTableName.substring(0, indexOfDot); } return schema; } private String pureTableName(String aTableName) { int indexOfDot = aTableName.indexOf("."); if (indexOfDot != -1) { return aTableName.substring(indexOfDot + 1); } else { return aTableName; } } public TablesFieldsCache lookupFieldsCache(String aSchema) { return schemasTablesFields.get(aSchema); } private TablesIndexesCache lookupIndexesCache(String aTableName) { String schema = schemaFromTableName(aTableName); return schemasTablesIndexes.get(schema); } @Override public Fields getTableMetadata(String aTableName) throws Exception { String schema = schemaFromTableName(aTableName); if (!schemasTablesFields.containsKey(schema)) { fillTablesCacheBySchema(schema); } TablesFieldsCache cache = lookupFieldsCache(schema); return cache != null ? cache.get(aTableName) : null; } public void refreshTableMetadata(String aTable) throws Exception { String schemaName = schemaFromTableName(aTable); TablesFieldsCache fieldsCache = schemasTablesFields.get(schemaName); if (fieldsCache == null) { fieldsCache = new TablesFieldsCache(); schemasTablesFields.put(schemaName, fieldsCache); } String tableName = schemaName != null && !schemaName.isEmpty() ? aTable.substring(schemaName.length() + 1) : aTable; Map<String, Fields> queried = fieldsCache.query(schemaName, tableName); fieldsCache.fill(schemaName, queried, Collections.singletonMap(tableName, null)); } public void removeTableMetadata(String aTableName) throws Exception { String schema = schemaFromTableName(aTableName); TablesFieldsCache cache = lookupFieldsCache(schema); if (cache != null) { cache.remove(aTableName); } } public void putTableMetadata(String aTableName, Fields aFields) throws Exception { String schema = schemaFromTableName(aTableName); TablesFieldsCache cache = lookupFieldsCache(schema); if (cache != null) { cache.put(aTableName, aFields); } } public boolean containsTableMetadata(String aTableName) throws Exception { return getTableMetadata(aTableName) != null; } public void removeTableIndexes(String aTableName) throws Exception { TablesIndexesCache cache = lookupIndexesCache(aTableName); if (cache != null) { cache.remove(aTableName); } } /** * Fills tables cache with fields, comments, keys (pk and fk) by connection * default schema. * * @throws Exception */ public final void fillTablesCacheByConnectionSchema() throws Exception { fillTablesCacheBySchema(null); } /** * Fills tables cache with fields, comments, keys (pk and fk). * * @param aSchema A schema for witch we should achieve metadata information. * If it is null, connection default schema is used * @throws Exception */ public void fillTablesCacheBySchema(String aSchema) throws Exception { String schema4Sql = aSchema != null && !aSchema.isEmpty() ? aSchema : getDatasourceSchema(); CallableConsumer<Map<String, String>, String> tablesReader = (aSchema4Sql) -> { DataSource ds = client.obtainDataSource(datasourceName); try (Connection conn = ds.getConnection()) { try (ResultSet r = conn.getMetaData().getTables(null, aSchema4Sql, null, new String[]{ClientConstants.JDBCPKS_TABLE_TYPE_TABLE, ClientConstants.JDBCPKS_TABLE_TYPE_VIEW})) { ColumnsIndicies idxs = new ColumnsIndicies(r.getMetaData()); int colIndex = idxs.find(ClientConstants.JDBCCOLS_TABLE_NAME); int colRemarks = idxs.find(ClientConstants.JDBCCOLS_REMARKS); assert colIndex > 0; assert colRemarks > 0; Map<String, String> tNames = new HashMap<>(); while (r.next()) { String lTableName = r.getString(colIndex); String lRemarks = r.getString(colRemarks); tNames.put(lTableName, lRemarks); } return tNames; } } }; Map<String, String> tablesNames = tablesReader.call(schema4Sql); if (schema4Sql != null && !schema4Sql.isEmpty() && tablesNames.isEmpty()) { tablesNames = tablesReader.call(schema4Sql.toLowerCase()); } if (schema4Sql != null && !schema4Sql.isEmpty() && tablesNames.isEmpty()) { tablesNames = tablesReader.call(schema4Sql.toUpperCase()); } TablesFieldsCache tablesFields = new TablesFieldsCache(); Map<String, Fields> queried = tablesFields.query(aSchema, tablesNames.keySet()); tablesFields.fill(aSchema, queried, tablesNames); schemasTablesFields.put(aSchema, tablesFields); } /** * Fills indexes cache. * * @param aSchema A schema for witch we should achieve metadata information. * If it is null, connection default schema is used * @param aTable * @throws Exception */ public void fillIndexesCacheByTable(String aSchema, String aTable) throws Exception { TablesIndexesCache tablesIndexes = schemasTablesIndexes.get(aSchema); if (tablesIndexes == null) { tablesIndexes = new TablesIndexesCache(); schemasTablesIndexes.put(aSchema, tablesIndexes); } DbTableIndexes tableIndexes = tablesIndexes.get(aTable); if (tableIndexes == null) { tableIndexes = tablesIndexes.query(aSchema, aTable); tablesIndexes.put(aTable, tableIndexes); } } public void clear() throws Exception { schemasTablesFields.clear(); schemasTablesIndexes.clear(); datasourceSchema = null; datasourceDriver = null; } public void removeSchema(String aSchema) { schemasTablesFields.remove(aSchema); schemasTablesIndexes.remove(aSchema); } public class TablesFieldsCache extends CaseInsesitiveMap<Fields> { public TablesFieldsCache() { super(); } protected void resolveKeys(String aSchema, Map<String, Fields> aTablesFields, Map<String, DbTableKeys> aTablesKeys) throws Exception { aTablesFields.keySet().stream().forEach((String lTableName) -> { Fields fields = aTablesFields.get(lTableName); DbTableKeys keys = aTablesKeys.get(lTableName); if (keys != null) { keys.getPks().entrySet().stream().forEach((Entry<String, PrimaryKeySpec> pkEntry) -> { Field f = fields.get(pkEntry.getKey()); if (f != null) { f.setPk(true); } }); keys.getFks().entrySet().stream().forEach((Entry<String, ForeignKeySpec> fkEntry) -> { Field f = fields.get(fkEntry.getKey()); if (f != null) { f.setFk(fkEntry.getValue()); } }); } }); } protected void fill(String aSchema, Map<String, Fields> aTablesFields, Map<String, String> aTablesDescriptions) throws Exception { if (aTablesFields != null && !aTablesFields.isEmpty()) { aTablesFields.keySet().stream().forEach((String lTableName) -> { Fields fields = aTablesFields.get(lTableName); fields.setTableDescription(aTablesDescriptions.get(lTableName)); String fullTableName = lTableName; if (aSchema != null && !aSchema.isEmpty()) { fullTableName = aSchema + "." + fullTableName; } put(fullTableName, fields); }); } } protected Map<String, Fields> query(String aSchema, Set<String> aTables) throws Exception { SqlDriver sqlDriver = getDatasourceSqlDriver(); String schema4Sql = aSchema != null && !aSchema.isEmpty() ? aSchema : getDatasourceSchema(); Map<String, Fields> columns; DataSource ds = client.obtainDataSource(datasourceName); try (Connection conn = ds.getConnection()) { DatabaseMetaData meta = conn.getMetaData(); try (ResultSet r = meta.getColumns(null, schema4Sql, null, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } if (schema4Sql != null && !schema4Sql.isEmpty() && columns.isEmpty()) { schema4Sql = schema4Sql.toLowerCase(); try (ResultSet r = meta.getColumns(null, schema4Sql, null, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } } if (schema4Sql != null && !schema4Sql.isEmpty() && columns.isEmpty()) { schema4Sql = schema4Sql.toUpperCase(); try (ResultSet r = meta.getColumns(null, schema4Sql, null, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } } Map<String, DbTableKeys> keys = new CaseInsesitiveMap<>(); for (String aTable : aTables) { try (ResultSet r = meta.getPrimaryKeys(null, schema4Sql, aTable)) { DbTableKeys tableKeys = readTablesPrimaryKeys(r); keys.put(aTable, tableKeys); } try (ResultSet r = meta.getImportedKeys(null, schema4Sql, aTable)) { readTablesForeignKeys(r, aSchema, sqlDriver, keys); } } resolveKeys(aSchema, columns, keys); return columns; } } protected Map<String, Fields> query(String aSchema, String aTable) throws Exception { SqlDriver sqlDriver = getDatasourceSqlDriver(); String schema4Sql = aSchema != null && !aSchema.isEmpty() ? aSchema : getDatasourceSchema(); Map<String, Fields> columns; DataSource ds = client.obtainDataSource(datasourceName); try (Connection conn = ds.getConnection()) { DatabaseMetaData meta = conn.getMetaData(); try (ResultSet r = meta.getColumns(null, schema4Sql, aTable, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } if (schema4Sql != null && !schema4Sql.isEmpty() && columns.isEmpty()) { schema4Sql = schema4Sql.toLowerCase(); aTable = aTable.toLowerCase(); try (ResultSet r = meta.getColumns(null, schema4Sql, aTable, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } } if (schema4Sql != null && !schema4Sql.isEmpty() && columns.isEmpty()) { schema4Sql = schema4Sql.toUpperCase(); aTable = aTable.toUpperCase(); try (ResultSet r = meta.getColumns(null, schema4Sql, aTable, null)) { columns = readTablesColumns(r, aSchema, sqlDriver); } } Map<String, DbTableKeys> keys = new CaseInsesitiveMap<>(); try (ResultSet r = meta.getPrimaryKeys(null, schema4Sql, aTable)) { DbTableKeys tableKeys = readTablesPrimaryKeys(r); keys.put(aTable, tableKeys); } try (ResultSet r = meta.getImportedKeys(null, schema4Sql, aTable)) { readTablesForeignKeys(r, aSchema, sqlDriver, keys); } resolveKeys(aSchema, columns, keys); return columns; } } protected Map<String, Fields> readTablesColumns(ResultSet r, String aSchema, SqlDriver sqlDriver) throws Exception { Map<String, Fields> tabledFields = new CaseInsesitiveMap<>(); if (r != null) { ColumnsIndicies colIndicies = new ColumnsIndicies(r.getMetaData()); int JDBCCOLS_TABLE_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_TABLE_NAME); int JDBCCOLS_COLUMN_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_COLUMN_NAME); int JDBCCOLS_REMARKS_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_REMARKS); int JDBCCOLS_DATA_TYPE_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_DATA_TYPE); int JDBCCOLS_TYPE_NAME_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_TYPE_NAME); int JDBCCOLS_COLUMN_SIZE_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_COLUMN_SIZE); int JDBCCOLS_DECIMAL_DIGITS_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_DECIMAL_DIGITS); int JDBCCOLS_NUM_PREC_RADIX_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_NUM_PREC_RADIX); int JDBCCOLS_NULLABLE_INDEX = colIndicies.find(ClientConstants.JDBCCOLS_NULLABLE); while (r.next()) { String fTableName = r.getString(JDBCCOLS_TABLE_INDEX); Fields fields = tabledFields.get(fTableName); if (fields == null) { fields = new Fields(); tabledFields.put(fTableName, fields); } String fName = r.getString(JDBCCOLS_COLUMN_INDEX); String fDescription = r.getString(JDBCCOLS_REMARKS_INDEX); JdbcField field = new JdbcField(); field.setName(fName.toLowerCase()); field.setDescription(fDescription); field.setOriginalName(fName); String rdbmsTypeName = r.getString(JDBCCOLS_TYPE_NAME_INDEX); field.setType(rdbmsTypeName); int jdbcType = r.getInt(JDBCCOLS_DATA_TYPE_INDEX); field.setJdbcType(jdbcType); int size = r.getInt(JDBCCOLS_COLUMN_SIZE_INDEX); field.setSize(size); int scale = r.getInt(JDBCCOLS_DECIMAL_DIGITS_INDEX); field.setScale(scale); int precision = r.getInt(JDBCCOLS_NUM_PREC_RADIX_INDEX); field.setPrecision(precision); int nullable = r.getInt(JDBCCOLS_NULLABLE_INDEX); field.setNullable(nullable == ResultSetMetaData.columnNullable); field.setSchemaName(aSchema); field.setTableName(fTableName); // sqlDriver.getTypesResolver().resolveSize(field); fields.add(field); } } return tabledFields; } protected DbTableKeys readTablesPrimaryKeys(ResultSet r) throws Exception { DbTableKeys dbPksFks = new DbTableKeys(); if (r != null) { ColumnsIndicies colsIndicies = new ColumnsIndicies(r.getMetaData()); int JDBCPKS_TABLE_SCHEM_INDEX = colsIndicies.find(ClientConstants.JDBCPKS_TABLE_SCHEM); int JDBCPKS_TABLE_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCPKS_TABLE_NAME); int JDBCPKS_COLUMN_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCPKS_COLUMN_NAME); int JDBCPKS_CONSTRAINT_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCPKS_CONSTRAINT_NAME); while (r.next()) { String lpkSchema = r.getString(JDBCPKS_TABLE_SCHEM_INDEX); String lpkTableName = r.getString(JDBCPKS_TABLE_NAME_INDEX); String lpkField = r.getString(JDBCPKS_COLUMN_NAME_INDEX); String lpkName = r.getString(JDBCPKS_CONSTRAINT_NAME_INDEX); // dbPksFks.addPrimaryKey(lpkSchema, lpkTableName, lpkField, lpkName); } } return dbPksFks; } protected void readTablesForeignKeys(ResultSet r, String aSchema, SqlDriver aSqlDriver, Map<String, DbTableKeys> tabledKeys) throws Exception { if (r != null) { ColumnsIndicies colsIndicies = new ColumnsIndicies(r.getMetaData()); int JDBCFKS_FKTABLE_SCHEM_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKTABLE_SCHEM); int JDBCFKS_FKTABLE_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKTABLE_NAME); int JDBCFKS_FKCOLUMN_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKCOLUMN_NAME); int JDBCFKS_FK_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FK_NAME); int JDBCFKS_FKUPDATE_RULE_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKUPDATE_RULE); int JDBCFKS_FKDELETE_RULE_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKDELETE_RULE); int JDBCFKS_FKDEFERRABILITY_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKDEFERRABILITY); // int JDBCFKS_FKPKTABLE_SCHEM_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKPKTABLE_SCHEM); int JDBCFKS_FKPKTABLE_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKPKTABLE_NAME); int JDBCFKS_FKPKCOLUMN_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKPKCOLUMN_NAME); int JDBCFKS_FKPK_NAME_INDEX = colsIndicies.find(ClientConstants.JDBCFKS_FKPK_NAME); while (r.next()) { String lfkSchema = r.getString(JDBCFKS_FKTABLE_SCHEM_INDEX); String lfkTableName = r.getString(JDBCFKS_FKTABLE_NAME_INDEX); String lfkField = r.getString(JDBCFKS_FKCOLUMN_NAME_INDEX); String lfkName = r.getString(JDBCFKS_FK_NAME_INDEX); short lfkUpdateRule = r.getShort(JDBCFKS_FKUPDATE_RULE_INDEX); short lfkDeleteRule = r.getShort(JDBCFKS_FKDELETE_RULE_INDEX); short lfkDeferability = r.getShort(JDBCFKS_FKDEFERRABILITY_INDEX); // String lpkSchema = r.getString(JDBCFKS_FKPKTABLE_SCHEM_INDEX); String lpkTableName = r.getString(JDBCFKS_FKPKTABLE_NAME_INDEX); String lpkField = r.getString(JDBCFKS_FKPKCOLUMN_NAME_INDEX); String lpkName = r.getString(JDBCFKS_FKPK_NAME_INDEX); // DbTableKeys dbPksFks = tabledKeys.get(lfkTableName); if (dbPksFks == null) { dbPksFks = new DbTableKeys(); tabledKeys.put(lfkTableName, dbPksFks); } dbPksFks.addForeignKey(lfkSchema, lfkTableName, lfkField, lfkName, ForeignKeySpec.ForeignKeyRule.valueOf(lfkUpdateRule), ForeignKeySpec.ForeignKeyRule.valueOf(lfkDeleteRule), lfkDeferability == 5, lpkSchema, lpkTableName, lpkField, lpkName); } } } } public boolean containsTableIndexes(String aTableName) throws Exception { return getTableIndexes(aTableName) != null; } public DbTableIndexes getTableIndexes(String aTableName) throws Exception { String schema = schemaFromTableName(aTableName); String pureTableName = pureTableName(aTableName); fillIndexesCacheByTable(schema, pureTableName); TablesIndexesCache cache = lookupIndexesCache(aTableName); return cache != null ? cache.get(aTableName) : null; } protected class TablesIndexesCache extends CaseInsesitiveMap<DbTableIndexes> { public TablesIndexesCache() { super(); } protected DbTableIndexes query(String aSchema, String aTable) throws Exception { DbTableIndexes tableIndexes = new DbTableIndexes(); String schema4Sql = aSchema != null && !aSchema.isEmpty() ? aSchema : getDatasourceSchema(); DataSource ds = client.obtainDataSource(datasourceName); try (Connection conn = ds.getConnection()) { try { try (ResultSet r = conn.getMetaData().getIndexInfo(null, schema4Sql, aTable, false, false)) { ColumnsIndicies idxs = new ColumnsIndicies(r.getMetaData()); int JDBCIDX_INDEX_NAME = idxs.find(ClientConstants.JDBCIDX_INDEX_NAME); int JDBCIDX_NON_UNIQUE = idxs.find(ClientConstants.JDBCIDX_NON_UNIQUE); int JDBCIDX_TYPE = idxs.find(ClientConstants.JDBCIDX_TYPE); //int JDBCIDX_TABLE_NAME = idxs.find(ClientConstants.JDBCIDX_TABLE_NAME); int JDBCIDX_COLUMN_NAME = idxs.find(ClientConstants.JDBCIDX_COLUMN_NAME); int JDBCIDX_ASC_OR_DESC = idxs.find(ClientConstants.JDBCIDX_ASC_OR_DESC); int JDBCIDX_ORDINAL_POSITION = idxs.find(ClientConstants.JDBCIDX_ORDINAL_POSITION); while (r.next()) { //String tableName = r.getString(JDBCIDX_TABLE_NAME); String idxName = r.getString(JDBCIDX_INDEX_NAME); if (!r.wasNull()) { DbTableIndexSpec idxSpec = tableIndexes.getIndexes().get(idxName); if (idxSpec == null) { idxSpec = new DbTableIndexSpec(); idxSpec.setName(idxName); tableIndexes.getIndexes().put(idxName, idxSpec); } boolean isUnique = r.getBoolean(JDBCIDX_NON_UNIQUE); idxSpec.setUnique(isUnique); short type = r.getShort(JDBCIDX_TYPE); idxSpec.setClustered(false); idxSpec.setHashed(false); switch (type) { case DatabaseMetaData.tableIndexClustered: idxSpec.setClustered(true); break; case DatabaseMetaData.tableIndexHashed: idxSpec.setHashed(true); break; case DatabaseMetaData.tableIndexStatistic: break; case DatabaseMetaData.tableIndexOther: break; } String sColumnName = r.getString(JDBCIDX_COLUMN_NAME); if (!r.wasNull()) { DbTableIndexColumnSpec column = idxSpec.getColumn(sColumnName); if (column == null) { column = new DbTableIndexColumnSpec(sColumnName, true); idxSpec.addColumn(column); } String sAsc = r.getString(JDBCIDX_ASC_OR_DESC); if (!r.wasNull()) { column.setAscending(sAsc.toLowerCase().equals("a")); } short sPosition = r.getShort(JDBCIDX_ORDINAL_POSITION); column.setOrdinalPosition((int) sPosition); } } } } } catch (SQLException ex) { Logger.getLogger(MetadataCache.class.getName()).log(Level.WARNING, ex.toString()); } } tableIndexes.sortIndexesColumns(); return tableIndexes; } } }