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