/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.ais.model; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import com.foundationdb.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DefaultNameGenerator implements NameGenerator { private static final Logger LOG = LoggerFactory.getLogger(DefaultNameGenerator.class); public static final int MAX_IDENT = 64; static final String IDENTITY_SEQUENCE_FORMAT = "%s_%s_seq"; // Use 1 as default offset because the AAM uses tableID 0 as a marker value. static final int USER_TABLE_ID_OFFSET = 1; static final int IS_TABLE_ID_OFFSET = 1000000000; private final Set<String> fullTextPaths; private final SortedSet<Integer> tableIDSet; private final SortedSet<Integer> isTableIDSet; private final Map<Integer,Integer> indexIDMap; private final Set<TableName> constraintNameSet; public DefaultNameGenerator() { this.fullTextPaths = new HashSet<>(); tableIDSet = new TreeSet<>(); isTableIDSet = new TreeSet<>(); indexIDMap = new HashMap<>(); constraintNameSet = new HashSet<>(); } public DefaultNameGenerator(AkibanInformationSchema ais) { this(); mergeAIS(ais); } protected synchronized int getMaxIndexID() { int max = 1; for(Integer id : indexIDMap.values()) { max = Math.max(max, id); } return max; } @Override public synchronized int generateTableID(TableName name) { final int offset; if(TableName.INFORMATION_SCHEMA.equals(name.getSchemaName())) { offset = getNextTableID(true); assert offset >= IS_TABLE_ID_OFFSET : "Offset too small for IS table " + name + ": " + offset; } else { offset = getNextTableID(false); if(offset >= IS_TABLE_ID_OFFSET) { LOG.warn("Offset for table {} unexpectedly large: {}", name, offset); } } return offset; } @Override public synchronized int generateIndexID(int rootTableID) { Integer current = indexIDMap.get(rootTableID); int newID = 1; if(current != null) { newID += current; } indexIDMap.put(rootTableID, newID); return newID; } @Override public synchronized TableName generateIdentitySequenceName(AkibanInformationSchema ais, TableName table, String column) { String proposed = String.format(IDENTITY_SEQUENCE_FORMAT, table.getTableName(), column); return findUnique(ais.getSequences().keySet(), new TableName(table.getSchemaName(), proposed)); } @Override public synchronized String generateJoinName(TableName parentTable, TableName childTable, String[] pkColNames, String [] fkColNames) { List<String> pkColNamesList = new LinkedList<>(); List<String> fkColNamesList = new LinkedList<>(); for (String col : pkColNames) { pkColNamesList.add(col); } for (String col : fkColNames) { fkColNamesList.add(col); } return generateJoinName(parentTable, childTable, pkColNamesList, fkColNamesList); } @Override public synchronized String generateJoinName(TableName parentTable, TableName childTable, List<JoinColumn> columns) { List<String> pkColNames = new LinkedList<>(); List<String> fkColNames = new LinkedList<>(); for (JoinColumn col : columns) { pkColNames.add(col.getParent().getName()); fkColNames.add(col.getChild().getName()); } return generateJoinName(parentTable, childTable, pkColNames, fkColNames); } @Override public synchronized String generateJoinName(TableName parentTable, TableName childTable, List<String> pkColNames, List<String> fkColNames) { String ret = String.format("%s/%s/%s/%s/%s/%s", parentTable.getSchemaName(), parentTable.getTableName(), Strings.join(pkColNames, ","), childTable.getSchemaName(), childTable, // TODO: This should be getTableName(), but preserve old behavior for test existing output Strings.join(fkColNames, ",")); String generatedName = ret.replace(',', '_'); TableName constrName = findUnique(constraintNameSet, new TableName(parentTable.getSchemaName(), generatedName)); Boolean newConstraintName = constraintNameSet.add(constrName); assert(newConstraintName); return constrName.getTableName(); } @Override public synchronized String generateFullTextIndexPath(FullTextIndex index) { IndexName name = index.getIndexName(); String proposed = String.format("%s.%s.%s", name.getSchemaName(), name.getTableName(), name.getName()); return makeUnique(fullTextPaths, proposed, MAX_IDENT); } @Override public synchronized TableName generateFKConstraintName(String schemaName, String tableName) { return generateConstraintName(schemaName, tableName, "fkey"); } @Override public synchronized TableName generatePKConstraintName( String schemaName, String tableName) { return generateConstraintName(schemaName, tableName, "pkey"); } @Override public synchronized TableName generateUniqueConstraintName( String schemaName, String tableName) { return generateConstraintName(schemaName, tableName, "ukey"); } private TableName generateConstraintName(String schemaName, String tableName, String postfix) { String proposed = String.format("%s_%s", tableName, postfix); TableName constrName = findUnique(constraintNameSet, new TableName(schemaName, proposed)); Boolean newConstraintName = constraintNameSet.add(constrName); assert(newConstraintName); return constrName; } @Override public synchronized void mergeAIS(AkibanInformationSchema ais) { isTableIDSet.addAll(collectTableIDs(ais, true)); tableIDSet.addAll(collectTableIDs(ais, false)); indexIDMap.putAll(collectMaxIndexIDs(ais)); constraintNameSet.addAll(ais.getConstraints().keySet()); } @Override public synchronized void removeTableID(int tableID) { isTableIDSet.remove(tableID); tableIDSet.remove(tableID); } /** Should be over-ridden by derived. */ @Override public synchronized Set<String> getStorageNames() { return Collections.emptySet(); } // // Private // /** * Get the next number that could be used for a table ID. The parameter indicates * where to start the search, but the ID will be unique across ALL tables. * @param isISTable Offset to start the search at. * @return Unique ID value. */ private int getNextTableID(boolean isISTable) { int nextID; if(isISTable) { nextID = isTableIDSet.isEmpty() ? IS_TABLE_ID_OFFSET : isTableIDSet.last() + 1; } else { nextID = tableIDSet.isEmpty() ? USER_TABLE_ID_OFFSET : tableIDSet.last() + 1; } while(isTableIDSet.contains(nextID) || tableIDSet.contains(nextID)) { nextID += 1; } if(isISTable) { isTableIDSet.add(nextID); } else { tableIDSet.add(nextID); } return nextID; } // // Static // private static SortedSet<Integer> collectTableIDs(AkibanInformationSchema ais, boolean onlyISTables) { SortedSet<Integer> idSet = new TreeSet<>(); for(Schema schema : ais.getSchemas().values()) { if(TableName.INFORMATION_SCHEMA.equals(schema.getName()) != onlyISTables) { continue; } for(Table table : schema.getTables().values()) { idSet.add(table.getTableId()); } } return idSet; } public static Map<Integer,Integer> collectMaxIndexIDs(AkibanInformationSchema ais) { MaxIndexIDVisitor visitor = new MaxIndexIDVisitor(); Map<Integer,Integer> idMap = new HashMap<>(); for(Group group : ais.getGroups().values()) { visitor.reset(); group.visit(visitor); idMap.put(group.getRoot().getTableId(), visitor.getMaxIndexID()); } return idMap; } /** Find a name that would be unique if added to {@code set}. */ private static TableName findUnique(Collection<TableName> set, TableName original) { int counter = 1; String baseName = original.getTableName(); TableName proposed = original; while(set.contains(proposed) || (proposed.getTableName().length() > MAX_IDENT)) { String countStr = "$" + counter++; int diff = baseName.length() + countStr.length() - MAX_IDENT; if(diff > 0) { baseName = truncate(baseName, baseName.length() - diff); } proposed = new TableName(original.getSchemaName(), baseName + countStr); } return proposed; } public static String findUnique(Collection<String> set, String original, int maxLength) { int counter = 1; String baseName = original; String proposed = original; while(set.contains(proposed) || (proposed.length() > maxLength)) { String countStr = "$" + counter++; int diff = baseName.length() + countStr.length() - maxLength; if(diff > 0) { baseName = truncate(baseName, baseName.length() - diff); } proposed = baseName + countStr; } return proposed; } public static String makeUnique(Collection<String> treeNames, String proposed, int maxLength) { String actual = findUnique(treeNames, proposed, maxLength); treeNames.add(actual); return actual; } public static String schemaNameForIndex(Index index) { switch(index.getIndexType()) { case TABLE: return ((TableIndex)index).getTable().getName().getSchemaName(); case GROUP: return ((GroupIndex)index).getGroup().getSchemaName(); case FULL_TEXT: return ((FullTextIndex)index).getIndexedTable().getName().getSchemaName(); default: throw new IllegalArgumentException("Unknown type: " + index.getIndexType()); } } private static String truncate(String s, int maxLen) { return (s.length() > maxLen) ? s.substring(0, maxLen) : s; } private static class MaxIndexIDVisitor extends AbstractVisitor { private int maxID; public MaxIndexIDVisitor() { } public void reset() { maxID = 0; } public int getMaxIndexID() { return maxID; } @Override public void visit(Index index) { maxID = Math.max(maxID, index.getIndexId()); } } }