001/* 002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.sql.jdbc.db; 020 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.LinkedHashMap; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Map; 030 031import org.nuxeo.ecm.core.storage.sql.ColumnType; 032import org.nuxeo.ecm.core.storage.sql.Model; 033import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; 034 035/** 036 * The basic implementation of a SQL table. 037 */ 038public class TableImpl implements Table { 039 040 private static final long serialVersionUID = 1L; 041 042 protected final Dialect dialect; 043 044 protected final String key; 045 046 protected final String name; 047 048 /** Map of logical names to columns. */ 049 private final LinkedHashMap<String, Column> columns; 050 051 private Column primaryColumn; 052 053 /** Logical names of indexed columns. */ 054 private final List<String[]> indexedColumns; 055 056 /** Index names. */ 057 private final Map<String[], String> indexNames; 058 059 /** Index types. */ 060 private final Map<String[], IndexType> indexTypes; 061 062 private boolean hasFulltextIndex; 063 064 /** 065 * Creates a new empty table. 066 */ 067 public TableImpl(Dialect dialect, String name, String key) { 068 this.dialect = dialect; 069 this.key = key; // Model table name 070 this.name = name; 071 // we use a LinkedHashMap to have deterministic ordering 072 columns = new LinkedHashMap<String, Column>(); 073 indexedColumns = new LinkedList<String[]>(); 074 indexNames = new HashMap<String[], String>(); 075 indexTypes = new HashMap<String[], IndexType>(); 076 } 077 078 @Override 079 public boolean isAlias() { 080 return false; 081 } 082 083 @Override 084 public Table getRealTable() { 085 return this; 086 } 087 088 @Override 089 public Dialect getDialect() { 090 return dialect; 091 } 092 093 @Override 094 public String getKey() { 095 return key; 096 } 097 098 @Override 099 public String getPhysicalName() { 100 return name; 101 } 102 103 @Override 104 public String getQuotedName() { 105 return dialect.openQuote() + name + dialect.closeQuote(); 106 } 107 108 @Override 109 public String getQuotedSuffixedName(String suffix) { 110 return dialect.openQuote() + name + suffix + dialect.closeQuote(); 111 } 112 113 @Override 114 public Column getColumn(String name) { 115 return columns.get(name); 116 } 117 118 @Override 119 public Column getPrimaryColumn() { 120 if (primaryColumn == null) { 121 for (Column column : columns.values()) { 122 if (column.isPrimary()) { 123 primaryColumn = column; 124 break; 125 } 126 } 127 } 128 return primaryColumn; 129 } 130 131 @Override 132 public Collection<Column> getColumns() { 133 return columns.values(); 134 } 135 136 /** 137 * Adds a column without dialect physical name canonicalization (for directories). 138 */ 139 public Column addColumn(String name, Column column) { 140 if (columns.containsKey(name)) { 141 throw new IllegalArgumentException("duplicate column " + name); 142 } 143 columns.put(name, column); 144 return column; 145 } 146 147 @Override 148 public Column addColumn(String name, ColumnType type, String key, Model model) { 149 String physicalName = dialect.getColumnName(name); 150 Column column = new Column(this, physicalName, type, key); 151 return addColumn(name, column); 152 } 153 154 /** 155 * Adds an index on one or several columns. 156 * 157 * @param columnNames the column names 158 */ 159 @Override 160 public void addIndex(String... columnNames) { 161 indexedColumns.add(columnNames); 162 } 163 164 @Override 165 public void addIndex(String indexName, IndexType indexType, String... columnNames) { 166 addIndex(columnNames); 167 indexNames.put(columnNames, indexName); 168 indexTypes.put(columnNames, indexType); 169 if (indexType == IndexType.FULLTEXT) { 170 hasFulltextIndex = true; 171 } 172 } 173 174 @Override 175 public boolean hasFulltextIndex() { 176 return hasFulltextIndex; 177 } 178 179 /** 180 * Computes the SQL statement to create the table. 181 * 182 * @return the SQL create string. 183 */ 184 @Override 185 public String getCreateSql() { 186 StringBuilder buf = new StringBuilder(); 187 buf.append("CREATE TABLE "); 188 buf.append(getQuotedName()); 189 buf.append(" ("); 190 String custom = dialect.getCustomColumnDefinition(this); 191 if (custom != null) { 192 buf.append(custom); 193 buf.append(", "); 194 } 195 for (Iterator<Column> it = columns.values().iterator(); it.hasNext();) { 196 addOneColumn(buf, it.next()); 197 if (it.hasNext()) { 198 buf.append(", "); 199 } 200 } 201 // unique 202 // check 203 buf.append(')'); 204 buf.append(dialect.getTableTypeString(this)); 205 return buf.toString(); 206 } 207 208 /** 209 * Computes the SQL statement to alter a table and add a column to it. 210 * 211 * @param column the column to add 212 * @return the SQL alter table string 213 */ 214 @Override 215 public String getAddColumnSql(Column column) { 216 StringBuilder buf = new StringBuilder(); 217 buf.append("ALTER TABLE "); 218 buf.append(getQuotedName()); 219 buf.append(' '); 220 buf.append(dialect.getAddColumnString()); 221 buf.append(' '); 222 addOneColumn(buf, column); 223 return buf.toString(); 224 } 225 226 /** 227 * Adds to buf the column name and its type and constraints for create / alter. 228 */ 229 protected void addOneColumn(StringBuilder buf, Column column) { 230 buf.append(column.getQuotedName()); 231 buf.append(' '); 232 buf.append(column.getSqlTypeString()); 233 String defaultValue = column.getDefaultValue(); 234 if (defaultValue != null) { 235 buf.append(" DEFAULT "); 236 buf.append(defaultValue); 237 } 238 if (column.isNullable()) { 239 buf.append(dialect.getNullColumnString()); 240 } else { 241 buf.append(" NOT NULL"); 242 } 243 } 244 245 @Override 246 public List<String> getPostCreateSqls(Model model) { 247 List<String> sqls = new LinkedList<String>(); 248 List<String> custom = dialect.getCustomPostCreateSqls(this, model); 249 sqls.addAll(custom); 250 for (Column column : columns.values()) { 251 postAddColumn(column, sqls, model); 252 } 253 return sqls; 254 } 255 256 @Override 257 public List<String> getPostAddSqls(Column column, Model model) { 258 List<String> sqls = new LinkedList<String>(); 259 postAddColumn(column, sqls, model); 260 return sqls; 261 } 262 263 protected void postAddColumn(Column column, List<String> sqls, Model model) { 264 if (column.isPrimary() && !(column.isIdentity() && dialect.isIdentityAlreadyPrimary())) { 265 StringBuilder buf = new StringBuilder(); 266 String constraintName = dialect.openQuote() + dialect.getPrimaryKeyConstraintName(key) 267 + dialect.closeQuote(); 268 buf.append("ALTER TABLE "); 269 buf.append(getQuotedName()); 270 buf.append(dialect.getAddPrimaryKeyConstraintString(constraintName)); 271 buf.append('('); 272 buf.append(column.getQuotedName()); 273 buf.append(')'); 274 sqls.add(buf.toString()); 275 } 276 if (column.isIdentity()) { 277 // Oracle needs a sequence + trigger 278 sqls.addAll(dialect.getPostCreateIdentityColumnSql(column)); 279 } 280 Table ft = column.getForeignTable(); 281 if (ft != null) { 282 Column fc = ft.getColumn(column.getForeignKey()); 283 String constraintName = dialect.openQuote() 284 + dialect.getForeignKeyConstraintName(key, column.getPhysicalName(), ft.getPhysicalName()) 285 + dialect.closeQuote(); 286 StringBuilder buf = new StringBuilder(); 287 buf.append("ALTER TABLE "); 288 buf.append(getQuotedName()); 289 buf.append(dialect.getAddForeignKeyConstraintString(constraintName, 290 new String[] { column.getQuotedName() }, ft.getQuotedName(), new String[] { fc.getQuotedName() }, 291 true)); 292 if (dialect.supportsCircularCascadeDeleteConstraints() 293 || (Model.MAIN_KEY.equals(fc.getPhysicalName()) && Model.MAIN_KEY.equals(column.getPhysicalName()))) { 294 // MS SQL Server can't have circular ON DELETE CASCADE. 295 // Use a trigger INSTEAD OF DELETE to cascade deletes 296 // recursively for: 297 // - hierarchy.parentid 298 // - proxies.targetid 299 buf.append(" ON DELETE CASCADE"); 300 } 301 sqls.add(buf.toString()); 302 } 303 // add indexes for this column 304 String columnName = column.getKey(); 305 INDEXES: // 306 for (String[] columnNames : indexedColumns) { 307 List<String> names = new ArrayList<String>(Arrays.asList(columnNames)); 308 // check that column is part of this index 309 if (!names.contains(columnName)) { 310 continue; 311 } 312 // check that column is the last one mentioned 313 for (Column c : getColumns()) { 314 String key = c.getKey(); 315 names.remove(key); 316 if (names.isEmpty()) { 317 // last one? 318 if (!columnName.equals(key)) { 319 continue INDEXES; 320 } 321 break; 322 } 323 } 324 // add this index now, as all columns have been created 325 List<Column> cols = new ArrayList<Column>(columnNames.length); 326 for (String name : columnNames) { 327 Column col = getColumn(name); 328 cols.add(col); 329 } 330 String indexName = indexNames.get(columnNames); 331 IndexType indexType = indexTypes.get(columnNames); 332 String createIndexSql = dialect.getCreateIndexSql(indexName, indexType, this, cols, model); 333 sqls.add(createIndexSql); 334 } 335 } 336 337 /** 338 * Computes the SQL statement to drop the table. 339 * <p> 340 * TODO drop constraints and indexes 341 * 342 * @return the SQL drop string. 343 */ 344 @Override 345 public String getDropSql() { 346 StringBuilder buf = new StringBuilder(); 347 buf.append("DROP TABLE "); 348 if (dialect.supportsIfExistsBeforeTableName()) { 349 buf.append("IF EXISTS "); 350 } 351 buf.append(getQuotedName()); 352 buf.append(dialect.getCascadeDropConstraintsString()); 353 if (dialect.supportsIfExistsAfterTableName()) { 354 buf.append(" IF EXISTS"); 355 } 356 return buf.toString(); 357 } 358 359 @Override 360 public String toString() { 361 return "Table(" + name + ')'; 362 } 363 364}