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}