1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.command.ddl; |
7 | |
8 | import java.util.ArrayList; |
9 | import java.util.HashSet; |
10 | |
11 | import org.h2.api.ErrorCode; |
12 | import org.h2.command.CommandInterface; |
13 | import org.h2.constraint.Constraint; |
14 | import org.h2.constraint.ConstraintCheck; |
15 | import org.h2.constraint.ConstraintReferential; |
16 | import org.h2.constraint.ConstraintUnique; |
17 | import org.h2.engine.Constants; |
18 | import org.h2.engine.Database; |
19 | import org.h2.engine.Right; |
20 | import org.h2.engine.Session; |
21 | import org.h2.expression.Expression; |
22 | import org.h2.index.Index; |
23 | import org.h2.index.IndexType; |
24 | import org.h2.message.DbException; |
25 | import org.h2.schema.Schema; |
26 | import org.h2.table.Column; |
27 | import org.h2.table.IndexColumn; |
28 | import org.h2.table.Table; |
29 | import org.h2.table.TableFilter; |
30 | import org.h2.util.New; |
31 | |
32 | /** |
33 | * This class represents the statement |
34 | * ALTER TABLE ADD CONSTRAINT |
35 | */ |
36 | public class AlterTableAddConstraint extends SchemaCommand { |
37 | |
38 | private int type; |
39 | private String constraintName; |
40 | private String tableName; |
41 | private IndexColumn[] indexColumns; |
42 | private int deleteAction; |
43 | private int updateAction; |
44 | private Schema refSchema; |
45 | private String refTableName; |
46 | private IndexColumn[] refIndexColumns; |
47 | private Expression checkExpression; |
48 | private Index index, refIndex; |
49 | private String comment; |
50 | private boolean checkExisting; |
51 | private boolean primaryKeyHash; |
52 | private final boolean ifNotExists; |
53 | private ArrayList<Index> createdIndexes = New.arrayList(); |
54 | |
55 | public AlterTableAddConstraint(Session session, Schema schema, |
56 | boolean ifNotExists) { |
57 | super(session, schema); |
58 | this.ifNotExists = ifNotExists; |
59 | } |
60 | |
61 | private String generateConstraintName(Table table) { |
62 | if (constraintName == null) { |
63 | constraintName = getSchema().getUniqueConstraintName( |
64 | session, table); |
65 | } |
66 | return constraintName; |
67 | } |
68 | |
69 | @Override |
70 | public int update() { |
71 | try { |
72 | return tryUpdate(); |
73 | } catch (DbException e) { |
74 | for (Index index : createdIndexes) { |
75 | session.getDatabase().removeSchemaObject(session, index); |
76 | } |
77 | throw e; |
78 | } finally { |
79 | getSchema().freeUniqueName(constraintName); |
80 | } |
81 | } |
82 | |
83 | /** |
84 | * Try to execute the statement. |
85 | * |
86 | * @return the update count |
87 | */ |
88 | private int tryUpdate() { |
89 | if (!transactional) { |
90 | session.commit(true); |
91 | } |
92 | Database db = session.getDatabase(); |
93 | Table table = getSchema().getTableOrView(session, tableName); |
94 | if (getSchema().findConstraint(session, constraintName) != null) { |
95 | if (ifNotExists) { |
96 | return 0; |
97 | } |
98 | throw DbException.get(ErrorCode.CONSTRAINT_ALREADY_EXISTS_1, |
99 | constraintName); |
100 | } |
101 | session.getUser().checkRight(table, Right.ALL); |
102 | db.lockMeta(session); |
103 | table.lock(session, true, true); |
104 | Constraint constraint; |
105 | switch (type) { |
106 | case CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_PRIMARY_KEY: { |
107 | IndexColumn.mapColumns(indexColumns, table); |
108 | index = table.findPrimaryKey(); |
109 | ArrayList<Constraint> constraints = table.getConstraints(); |
110 | for (int i = 0; constraints != null && i < constraints.size(); i++) { |
111 | Constraint c = constraints.get(i); |
112 | if (Constraint.PRIMARY_KEY.equals(c.getConstraintType())) { |
113 | throw DbException.get(ErrorCode.SECOND_PRIMARY_KEY); |
114 | } |
115 | } |
116 | if (index != null) { |
117 | // if there is an index, it must match with the one declared |
118 | // we don't test ascending / descending |
119 | IndexColumn[] pkCols = index.getIndexColumns(); |
120 | if (pkCols.length != indexColumns.length) { |
121 | throw DbException.get(ErrorCode.SECOND_PRIMARY_KEY); |
122 | } |
123 | for (int i = 0; i < pkCols.length; i++) { |
124 | if (pkCols[i].column != indexColumns[i].column) { |
125 | throw DbException.get(ErrorCode.SECOND_PRIMARY_KEY); |
126 | } |
127 | } |
128 | } |
129 | if (index == null) { |
130 | IndexType indexType = IndexType.createPrimaryKey( |
131 | table.isPersistIndexes(), primaryKeyHash); |
132 | String indexName = table.getSchema().getUniqueIndexName( |
133 | session, table, Constants.PREFIX_PRIMARY_KEY); |
134 | int id = getObjectId(); |
135 | try { |
136 | index = table.addIndex(session, indexName, id, |
137 | indexColumns, indexType, true, null); |
138 | } finally { |
139 | getSchema().freeUniqueName(indexName); |
140 | } |
141 | } |
142 | index.getIndexType().setBelongsToConstraint(true); |
143 | int constraintId = getObjectId(); |
144 | String name = generateConstraintName(table); |
145 | ConstraintUnique pk = new ConstraintUnique(getSchema(), |
146 | constraintId, name, table, true); |
147 | pk.setColumns(indexColumns); |
148 | pk.setIndex(index, true); |
149 | constraint = pk; |
150 | break; |
151 | } |
152 | case CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE: { |
153 | IndexColumn.mapColumns(indexColumns, table); |
154 | boolean isOwner = false; |
155 | if (index != null && canUseUniqueIndex(index, table, indexColumns)) { |
156 | isOwner = true; |
157 | index.getIndexType().setBelongsToConstraint(true); |
158 | } else { |
159 | index = getUniqueIndex(table, indexColumns); |
160 | if (index == null) { |
161 | index = createIndex(table, indexColumns, true); |
162 | isOwner = true; |
163 | } |
164 | } |
165 | int id = getObjectId(); |
166 | String name = generateConstraintName(table); |
167 | ConstraintUnique unique = new ConstraintUnique(getSchema(), id, |
168 | name, table, false); |
169 | unique.setColumns(indexColumns); |
170 | unique.setIndex(index, isOwner); |
171 | constraint = unique; |
172 | break; |
173 | } |
174 | case CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_CHECK: { |
175 | int id = getObjectId(); |
176 | String name = generateConstraintName(table); |
177 | ConstraintCheck check = new ConstraintCheck(getSchema(), id, name, table); |
178 | TableFilter filter = new TableFilter(session, table, null, false, null); |
179 | checkExpression.mapColumns(filter, 0); |
180 | checkExpression = checkExpression.optimize(session); |
181 | check.setExpression(checkExpression); |
182 | check.setTableFilter(filter); |
183 | constraint = check; |
184 | if (checkExisting) { |
185 | check.checkExistingData(session); |
186 | } |
187 | break; |
188 | } |
189 | case CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_REFERENTIAL: { |
190 | Table refTable = refSchema.getTableOrView(session, refTableName); |
191 | session.getUser().checkRight(refTable, Right.ALL); |
192 | if (!refTable.canReference()) { |
193 | throw DbException.getUnsupportedException("Reference " + |
194 | refTable.getSQL()); |
195 | } |
196 | boolean isOwner = false; |
197 | IndexColumn.mapColumns(indexColumns, table); |
198 | if (index != null && canUseIndex(index, table, indexColumns, false)) { |
199 | isOwner = true; |
200 | index.getIndexType().setBelongsToConstraint(true); |
201 | } else { |
202 | if (db.isStarting()) { |
203 | // before version 1.3.176, an existing index was used: |
204 | // must do the same to avoid |
205 | // Unique index or primary key violation: |
206 | // "PRIMARY KEY ON """".PAGE_INDEX" |
207 | index = getIndex(table, indexColumns, true); |
208 | } else { |
209 | index = getIndex(table, indexColumns, false); |
210 | } |
211 | if (index == null) { |
212 | index = createIndex(table, indexColumns, false); |
213 | isOwner = true; |
214 | } |
215 | } |
216 | if (refIndexColumns == null) { |
217 | Index refIdx = refTable.getPrimaryKey(); |
218 | refIndexColumns = refIdx.getIndexColumns(); |
219 | } else { |
220 | IndexColumn.mapColumns(refIndexColumns, refTable); |
221 | } |
222 | if (refIndexColumns.length != indexColumns.length) { |
223 | throw DbException.get(ErrorCode.COLUMN_COUNT_DOES_NOT_MATCH); |
224 | } |
225 | boolean isRefOwner = false; |
226 | if (refIndex != null && refIndex.getTable() == refTable && |
227 | canUseIndex(refIndex, refTable, refIndexColumns, false)) { |
228 | isRefOwner = true; |
229 | refIndex.getIndexType().setBelongsToConstraint(true); |
230 | } else { |
231 | refIndex = null; |
232 | } |
233 | if (refIndex == null) { |
234 | refIndex = getIndex(refTable, refIndexColumns, false); |
235 | if (refIndex == null) { |
236 | refIndex = createIndex(refTable, refIndexColumns, true); |
237 | isRefOwner = true; |
238 | } |
239 | } |
240 | int id = getObjectId(); |
241 | String name = generateConstraintName(table); |
242 | ConstraintReferential ref = new ConstraintReferential(getSchema(), |
243 | id, name, table); |
244 | ref.setColumns(indexColumns); |
245 | ref.setIndex(index, isOwner); |
246 | ref.setRefTable(refTable); |
247 | ref.setRefColumns(refIndexColumns); |
248 | ref.setRefIndex(refIndex, isRefOwner); |
249 | if (checkExisting) { |
250 | ref.checkExistingData(session); |
251 | } |
252 | constraint = ref; |
253 | refTable.addConstraint(constraint); |
254 | ref.setDeleteAction(deleteAction); |
255 | ref.setUpdateAction(updateAction); |
256 | break; |
257 | } |
258 | default: |
259 | throw DbException.throwInternalError("type=" + type); |
260 | } |
261 | // parent relationship is already set with addConstraint |
262 | constraint.setComment(comment); |
263 | if (table.isTemporary() && !table.isGlobalTemporary()) { |
264 | session.addLocalTempTableConstraint(constraint); |
265 | } else { |
266 | db.addSchemaObject(session, constraint); |
267 | } |
268 | table.addConstraint(constraint); |
269 | return 0; |
270 | } |
271 | |
272 | private Index createIndex(Table t, IndexColumn[] cols, boolean unique) { |
273 | int indexId = getObjectId(); |
274 | IndexType indexType; |
275 | if (unique) { |
276 | // for unique constraints |
277 | indexType = IndexType.createUnique(t.isPersistIndexes(), false); |
278 | } else { |
279 | // constraints |
280 | indexType = IndexType.createNonUnique(t.isPersistIndexes()); |
281 | } |
282 | indexType.setBelongsToConstraint(true); |
283 | String prefix = constraintName == null ? "CONSTRAINT" : constraintName; |
284 | String indexName = t.getSchema().getUniqueIndexName(session, t, |
285 | prefix + "_INDEX_"); |
286 | try { |
287 | Index index = t.addIndex(session, indexName, indexId, cols, |
288 | indexType, true, null); |
289 | createdIndexes.add(index); |
290 | return index; |
291 | } finally { |
292 | getSchema().freeUniqueName(indexName); |
293 | } |
294 | } |
295 | |
296 | public void setDeleteAction(int action) { |
297 | this.deleteAction = action; |
298 | } |
299 | |
300 | public void setUpdateAction(int action) { |
301 | this.updateAction = action; |
302 | } |
303 | |
304 | private static Index getUniqueIndex(Table t, IndexColumn[] cols) { |
305 | for (Index idx : t.getIndexes()) { |
306 | if (canUseUniqueIndex(idx, t, cols)) { |
307 | return idx; |
308 | } |
309 | } |
310 | return null; |
311 | } |
312 | |
313 | private static Index getIndex(Table t, IndexColumn[] cols, boolean moreColumnOk) { |
314 | for (Index idx : t.getIndexes()) { |
315 | if (canUseIndex(idx, t, cols, moreColumnOk)) { |
316 | return idx; |
317 | } |
318 | } |
319 | return null; |
320 | } |
321 | |
322 | private static boolean canUseUniqueIndex(Index idx, Table table, |
323 | IndexColumn[] cols) { |
324 | if (idx.getTable() != table || !idx.getIndexType().isUnique()) { |
325 | return false; |
326 | } |
327 | Column[] indexCols = idx.getColumns(); |
328 | if (indexCols.length > cols.length) { |
329 | return false; |
330 | } |
331 | HashSet<Column> set = New.hashSet(); |
332 | for (IndexColumn c : cols) { |
333 | set.add(c.column); |
334 | } |
335 | for (Column c : indexCols) { |
336 | // all columns of the index must be part of the list, |
337 | // but not all columns of the list need to be part of the index |
338 | if (!set.contains(c)) { |
339 | return false; |
340 | } |
341 | } |
342 | return true; |
343 | } |
344 | |
345 | private static boolean canUseIndex(Index existingIndex, Table table, |
346 | IndexColumn[] cols, boolean moreColumnsOk) { |
347 | if (existingIndex.getTable() != table || existingIndex.getCreateSQL() == null) { |
348 | // can't use the scan index or index of another table |
349 | return false; |
350 | } |
351 | Column[] indexCols = existingIndex.getColumns(); |
352 | |
353 | if (moreColumnsOk) { |
354 | if (indexCols.length < cols.length) { |
355 | return false; |
356 | } |
357 | for (IndexColumn col : cols) { |
358 | // all columns of the list must be part of the index, |
359 | // but not all columns of the index need to be part of the list |
360 | // holes are not allowed (index=a,b,c & list=a,b is ok; |
361 | // but list=a,c is not) |
362 | int idx = existingIndex.getColumnIndex(col.column); |
363 | if (idx < 0 || idx >= cols.length) { |
364 | return false; |
365 | } |
366 | } |
367 | } else { |
368 | if (indexCols.length != cols.length) { |
369 | return false; |
370 | } |
371 | for (IndexColumn col : cols) { |
372 | // all columns of the list must be part of the index |
373 | int idx = existingIndex.getColumnIndex(col.column); |
374 | if (idx < 0) { |
375 | return false; |
376 | } |
377 | } |
378 | } |
379 | return true; |
380 | } |
381 | |
382 | public void setConstraintName(String constraintName) { |
383 | this.constraintName = constraintName; |
384 | } |
385 | |
386 | public void setType(int type) { |
387 | this.type = type; |
388 | } |
389 | |
390 | @Override |
391 | public int getType() { |
392 | return type; |
393 | } |
394 | |
395 | public void setCheckExpression(Expression expression) { |
396 | this.checkExpression = expression; |
397 | } |
398 | |
399 | public void setTableName(String tableName) { |
400 | this.tableName = tableName; |
401 | } |
402 | |
403 | public void setIndexColumns(IndexColumn[] indexColumns) { |
404 | this.indexColumns = indexColumns; |
405 | } |
406 | |
407 | public IndexColumn[] getIndexColumns() { |
408 | return indexColumns; |
409 | } |
410 | |
411 | /** |
412 | * Set the referenced table. |
413 | * |
414 | * @param refSchema the schema |
415 | * @param ref the table name |
416 | */ |
417 | public void setRefTableName(Schema refSchema, String ref) { |
418 | this.refSchema = refSchema; |
419 | this.refTableName = ref; |
420 | } |
421 | |
422 | public void setRefIndexColumns(IndexColumn[] indexColumns) { |
423 | this.refIndexColumns = indexColumns; |
424 | } |
425 | |
426 | public void setIndex(Index index) { |
427 | this.index = index; |
428 | } |
429 | |
430 | public void setRefIndex(Index refIndex) { |
431 | this.refIndex = refIndex; |
432 | } |
433 | |
434 | public void setComment(String comment) { |
435 | this.comment = comment; |
436 | } |
437 | |
438 | public void setCheckExisting(boolean b) { |
439 | this.checkExisting = b; |
440 | } |
441 | |
442 | public void setPrimaryKeyHash(boolean b) { |
443 | this.primaryKeyHash = b; |
444 | } |
445 | |
446 | } |