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.constraint; |
7 | |
8 | import java.util.ArrayList; |
9 | import java.util.HashSet; |
10 | |
11 | import org.h2.api.ErrorCode; |
12 | import org.h2.command.Parser; |
13 | import org.h2.command.Prepared; |
14 | import org.h2.engine.Session; |
15 | import org.h2.expression.Expression; |
16 | import org.h2.expression.Parameter; |
17 | import org.h2.index.Cursor; |
18 | import org.h2.index.Index; |
19 | import org.h2.message.DbException; |
20 | import org.h2.result.ResultInterface; |
21 | import org.h2.result.Row; |
22 | import org.h2.result.SearchRow; |
23 | import org.h2.schema.Schema; |
24 | import org.h2.table.Column; |
25 | import org.h2.table.IndexColumn; |
26 | import org.h2.table.Table; |
27 | import org.h2.util.New; |
28 | import org.h2.util.StatementBuilder; |
29 | import org.h2.util.StringUtils; |
30 | import org.h2.value.Value; |
31 | import org.h2.value.ValueNull; |
32 | |
33 | /** |
34 | * A referential constraint. |
35 | */ |
36 | public class ConstraintReferential extends Constraint { |
37 | |
38 | /** |
39 | * The action is to restrict the operation. |
40 | */ |
41 | public static final int RESTRICT = 0; |
42 | |
43 | /** |
44 | * The action is to cascade the operation. |
45 | */ |
46 | public static final int CASCADE = 1; |
47 | |
48 | /** |
49 | * The action is to set the value to the default value. |
50 | */ |
51 | public static final int SET_DEFAULT = 2; |
52 | |
53 | /** |
54 | * The action is to set the value to NULL. |
55 | */ |
56 | public static final int SET_NULL = 3; |
57 | |
58 | private IndexColumn[] columns; |
59 | private IndexColumn[] refColumns; |
60 | private int deleteAction; |
61 | private int updateAction; |
62 | private Table refTable; |
63 | private Index index; |
64 | private Index refIndex; |
65 | private boolean indexOwner; |
66 | private boolean refIndexOwner; |
67 | private String deleteSQL, updateSQL; |
68 | private boolean skipOwnTable; |
69 | |
70 | public ConstraintReferential(Schema schema, int id, String name, Table table) { |
71 | super(schema, id, name, table); |
72 | } |
73 | |
74 | @Override |
75 | public String getConstraintType() { |
76 | return Constraint.REFERENTIAL; |
77 | } |
78 | |
79 | private static void appendAction(StatementBuilder buff, int action) { |
80 | switch (action) { |
81 | case CASCADE: |
82 | buff.append("CASCADE"); |
83 | break; |
84 | case SET_DEFAULT: |
85 | buff.append("SET DEFAULT"); |
86 | break; |
87 | case SET_NULL: |
88 | buff.append("SET NULL"); |
89 | break; |
90 | default: |
91 | DbException.throwInternalError("action=" + action); |
92 | } |
93 | } |
94 | |
95 | /** |
96 | * Create the SQL statement of this object so a copy of the table can be |
97 | * made. |
98 | * |
99 | * @param forTable the table to create the object for |
100 | * @param quotedName the name of this object (quoted if necessary) |
101 | * @return the SQL statement |
102 | */ |
103 | @Override |
104 | public String getCreateSQLForCopy(Table forTable, String quotedName) { |
105 | return getCreateSQLForCopy(forTable, refTable, quotedName, true); |
106 | } |
107 | |
108 | /** |
109 | * Create the SQL statement of this object so a copy of the table can be |
110 | * made. |
111 | * |
112 | * @param forTable the table to create the object for |
113 | * @param forRefTable the referenced table |
114 | * @param quotedName the name of this object (quoted if necessary) |
115 | * @param internalIndex add the index name to the statement |
116 | * @return the SQL statement |
117 | */ |
118 | public String getCreateSQLForCopy(Table forTable, Table forRefTable, |
119 | String quotedName, boolean internalIndex) { |
120 | StatementBuilder buff = new StatementBuilder("ALTER TABLE "); |
121 | String mainTable = forTable.getSQL(); |
122 | buff.append(mainTable).append(" ADD CONSTRAINT "); |
123 | if (forTable.isHidden()) { |
124 | buff.append("IF NOT EXISTS "); |
125 | } |
126 | buff.append(quotedName); |
127 | if (comment != null) { |
128 | buff.append(" COMMENT ").append(StringUtils.quoteStringSQL(comment)); |
129 | } |
130 | IndexColumn[] cols = columns; |
131 | IndexColumn[] refCols = refColumns; |
132 | buff.append(" FOREIGN KEY("); |
133 | for (IndexColumn c : cols) { |
134 | buff.appendExceptFirst(", "); |
135 | buff.append(c.getSQL()); |
136 | } |
137 | buff.append(')'); |
138 | if (internalIndex && indexOwner && forTable == this.table) { |
139 | buff.append(" INDEX ").append(index.getSQL()); |
140 | } |
141 | buff.append(" REFERENCES "); |
142 | String quotedRefTable; |
143 | if (this.table == this.refTable) { |
144 | // self-referencing constraints: need to use new table |
145 | quotedRefTable = forTable.getSQL(); |
146 | } else { |
147 | quotedRefTable = forRefTable.getSQL(); |
148 | } |
149 | buff.append(quotedRefTable).append('('); |
150 | buff.resetCount(); |
151 | for (IndexColumn r : refCols) { |
152 | buff.appendExceptFirst(", "); |
153 | buff.append(r.getSQL()); |
154 | } |
155 | buff.append(')'); |
156 | if (internalIndex && refIndexOwner && forTable == this.table) { |
157 | buff.append(" INDEX ").append(refIndex.getSQL()); |
158 | } |
159 | if (deleteAction != RESTRICT) { |
160 | buff.append(" ON DELETE "); |
161 | appendAction(buff, deleteAction); |
162 | } |
163 | if (updateAction != RESTRICT) { |
164 | buff.append(" ON UPDATE "); |
165 | appendAction(buff, updateAction); |
166 | } |
167 | return buff.append(" NOCHECK").toString(); |
168 | } |
169 | |
170 | |
171 | /** |
172 | * Get a short description of the constraint. This includes the constraint |
173 | * name (if set), and the constraint expression. |
174 | * |
175 | * @param searchIndex the index, or null |
176 | * @param check the row, or null |
177 | * @return the description |
178 | */ |
179 | private String getShortDescription(Index searchIndex, SearchRow check) { |
180 | StatementBuilder buff = new StatementBuilder(getName()); |
181 | buff.append(": ").append(table.getSQL()).append(" FOREIGN KEY("); |
182 | for (IndexColumn c : columns) { |
183 | buff.appendExceptFirst(", "); |
184 | buff.append(c.getSQL()); |
185 | } |
186 | buff.append(") REFERENCES ").append(refTable.getSQL()).append('('); |
187 | buff.resetCount(); |
188 | for (IndexColumn r : refColumns) { |
189 | buff.appendExceptFirst(", "); |
190 | buff.append(r.getSQL()); |
191 | } |
192 | buff.append(')'); |
193 | if (searchIndex != null && check != null) { |
194 | buff.append(" ("); |
195 | buff.resetCount(); |
196 | Column[] cols = searchIndex.getColumns(); |
197 | int len = Math.min(columns.length, cols.length); |
198 | for (int i = 0; i < len; i++) { |
199 | int idx = cols[i].getColumnId(); |
200 | Value c = check.getValue(idx); |
201 | buff.appendExceptFirst(", "); |
202 | buff.append(c == null ? "" : c.toString()); |
203 | } |
204 | buff.append(')'); |
205 | } |
206 | return buff.toString(); |
207 | } |
208 | |
209 | @Override |
210 | public String getCreateSQLWithoutIndexes() { |
211 | return getCreateSQLForCopy(table, refTable, getSQL(), false); |
212 | } |
213 | |
214 | @Override |
215 | public String getCreateSQL() { |
216 | return getCreateSQLForCopy(table, getSQL()); |
217 | } |
218 | |
219 | public void setColumns(IndexColumn[] cols) { |
220 | columns = cols; |
221 | } |
222 | |
223 | public IndexColumn[] getColumns() { |
224 | return columns; |
225 | } |
226 | |
227 | @Override |
228 | public HashSet<Column> getReferencedColumns(Table table) { |
229 | HashSet<Column> result = New.hashSet(); |
230 | if (table == this.table) { |
231 | for (IndexColumn c : columns) { |
232 | result.add(c.column); |
233 | } |
234 | } else if (table == this.refTable) { |
235 | for (IndexColumn c : refColumns) { |
236 | result.add(c.column); |
237 | } |
238 | } |
239 | return result; |
240 | } |
241 | |
242 | public void setRefColumns(IndexColumn[] refCols) { |
243 | refColumns = refCols; |
244 | } |
245 | |
246 | public IndexColumn[] getRefColumns() { |
247 | return refColumns; |
248 | } |
249 | |
250 | public void setRefTable(Table refTable) { |
251 | this.refTable = refTable; |
252 | if (refTable.isTemporary()) { |
253 | setTemporary(true); |
254 | } |
255 | } |
256 | |
257 | /** |
258 | * Set the index to use for this constraint. |
259 | * |
260 | * @param index the index |
261 | * @param isOwner true if the index is generated by the system and belongs |
262 | * to this constraint |
263 | */ |
264 | public void setIndex(Index index, boolean isOwner) { |
265 | this.index = index; |
266 | this.indexOwner = isOwner; |
267 | } |
268 | |
269 | /** |
270 | * Set the index of the referenced table to use for this constraint. |
271 | * |
272 | * @param refIndex the index |
273 | * @param isRefOwner true if the index is generated by the system and |
274 | * belongs to this constraint |
275 | */ |
276 | public void setRefIndex(Index refIndex, boolean isRefOwner) { |
277 | this.refIndex = refIndex; |
278 | this.refIndexOwner = isRefOwner; |
279 | } |
280 | |
281 | @Override |
282 | public void removeChildrenAndResources(Session session) { |
283 | table.removeConstraint(this); |
284 | refTable.removeConstraint(this); |
285 | if (indexOwner) { |
286 | table.removeIndexOrTransferOwnership(session, index); |
287 | } |
288 | if (refIndexOwner) { |
289 | refTable.removeIndexOrTransferOwnership(session, refIndex); |
290 | } |
291 | database.removeMeta(session, getId()); |
292 | refTable = null; |
293 | index = null; |
294 | refIndex = null; |
295 | columns = null; |
296 | refColumns = null; |
297 | deleteSQL = null; |
298 | updateSQL = null; |
299 | table = null; |
300 | invalidate(); |
301 | } |
302 | |
303 | @Override |
304 | public void checkRow(Session session, Table t, Row oldRow, Row newRow) { |
305 | if (!database.getReferentialIntegrity()) { |
306 | return; |
307 | } |
308 | if (!table.getCheckForeignKeyConstraints() || |
309 | !refTable.getCheckForeignKeyConstraints()) { |
310 | return; |
311 | } |
312 | if (t == table) { |
313 | if (!skipOwnTable) { |
314 | checkRowOwnTable(session, oldRow, newRow); |
315 | } |
316 | } |
317 | if (t == refTable) { |
318 | checkRowRefTable(session, oldRow, newRow); |
319 | } |
320 | } |
321 | |
322 | private void checkRowOwnTable(Session session, Row oldRow, Row newRow) { |
323 | if (newRow == null) { |
324 | return; |
325 | } |
326 | boolean constraintColumnsEqual = oldRow != null; |
327 | for (IndexColumn col : columns) { |
328 | int idx = col.column.getColumnId(); |
329 | Value v = newRow.getValue(idx); |
330 | if (v == ValueNull.INSTANCE) { |
331 | // return early if one of the columns is NULL |
332 | return; |
333 | } |
334 | if (constraintColumnsEqual) { |
335 | if (!database.areEqual(v, oldRow.getValue(idx))) { |
336 | constraintColumnsEqual = false; |
337 | } |
338 | } |
339 | } |
340 | if (constraintColumnsEqual) { |
341 | // return early if the key columns didn't change |
342 | return; |
343 | } |
344 | if (refTable == table) { |
345 | // special case self referencing constraints: |
346 | // check the inserted row first |
347 | boolean self = true; |
348 | for (int i = 0, len = columns.length; i < len; i++) { |
349 | int idx = columns[i].column.getColumnId(); |
350 | Value v = newRow.getValue(idx); |
351 | Column refCol = refColumns[i].column; |
352 | int refIdx = refCol.getColumnId(); |
353 | Value r = newRow.getValue(refIdx); |
354 | if (!database.areEqual(r, v)) { |
355 | self = false; |
356 | break; |
357 | } |
358 | } |
359 | if (self) { |
360 | return; |
361 | } |
362 | } |
363 | Row check = refTable.getTemplateRow(); |
364 | for (int i = 0, len = columns.length; i < len; i++) { |
365 | int idx = columns[i].column.getColumnId(); |
366 | Value v = newRow.getValue(idx); |
367 | Column refCol = refColumns[i].column; |
368 | int refIdx = refCol.getColumnId(); |
369 | check.setValue(refIdx, refCol.convert(v)); |
370 | } |
371 | if (!existsRow(session, refIndex, check, null)) { |
372 | throw DbException.get(ErrorCode.REFERENTIAL_INTEGRITY_VIOLATED_PARENT_MISSING_1, |
373 | getShortDescription(refIndex, check)); |
374 | } |
375 | } |
376 | |
377 | private boolean existsRow(Session session, Index searchIndex, |
378 | SearchRow check, Row excluding) { |
379 | Table searchTable = searchIndex.getTable(); |
380 | searchTable.lock(session, false, false); |
381 | Cursor cursor = searchIndex.find(session, check, check); |
382 | while (cursor.next()) { |
383 | SearchRow found; |
384 | found = cursor.getSearchRow(); |
385 | if (excluding != null && found.getKey() == excluding.getKey()) { |
386 | continue; |
387 | } |
388 | Column[] cols = searchIndex.getColumns(); |
389 | boolean allEqual = true; |
390 | int len = Math.min(columns.length, cols.length); |
391 | for (int i = 0; i < len; i++) { |
392 | int idx = cols[i].getColumnId(); |
393 | Value c = check.getValue(idx); |
394 | Value f = found.getValue(idx); |
395 | if (searchTable.compareTypeSave(c, f) != 0) { |
396 | allEqual = false; |
397 | break; |
398 | } |
399 | } |
400 | if (allEqual) { |
401 | return true; |
402 | } |
403 | } |
404 | return false; |
405 | } |
406 | |
407 | private boolean isEqual(Row oldRow, Row newRow) { |
408 | return refIndex.compareRows(oldRow, newRow) == 0; |
409 | } |
410 | |
411 | private void checkRow(Session session, Row oldRow) { |
412 | SearchRow check = table.getTemplateSimpleRow(false); |
413 | for (int i = 0, len = columns.length; i < len; i++) { |
414 | Column refCol = refColumns[i].column; |
415 | int refIdx = refCol.getColumnId(); |
416 | Column col = columns[i].column; |
417 | Value v = col.convert(oldRow.getValue(refIdx)); |
418 | if (v == ValueNull.INSTANCE) { |
419 | return; |
420 | } |
421 | check.setValue(col.getColumnId(), v); |
422 | } |
423 | // exclude the row only for self-referencing constraints |
424 | Row excluding = (refTable == table) ? oldRow : null; |
425 | if (existsRow(session, index, check, excluding)) { |
426 | throw DbException.get(ErrorCode.REFERENTIAL_INTEGRITY_VIOLATED_CHILD_EXISTS_1, |
427 | getShortDescription(index, check)); |
428 | } |
429 | } |
430 | |
431 | private void checkRowRefTable(Session session, Row oldRow, Row newRow) { |
432 | if (oldRow == null) { |
433 | // this is an insert |
434 | return; |
435 | } |
436 | if (newRow != null && isEqual(oldRow, newRow)) { |
437 | // on an update, if both old and new are the same, don't do anything |
438 | return; |
439 | } |
440 | if (newRow == null) { |
441 | // this is a delete |
442 | if (deleteAction == RESTRICT) { |
443 | checkRow(session, oldRow); |
444 | } else { |
445 | int i = deleteAction == CASCADE ? 0 : columns.length; |
446 | Prepared deleteCommand = getDelete(session); |
447 | setWhere(deleteCommand, i, oldRow); |
448 | updateWithSkipCheck(deleteCommand); |
449 | } |
450 | } else { |
451 | // this is an update |
452 | if (updateAction == RESTRICT) { |
453 | checkRow(session, oldRow); |
454 | } else { |
455 | Prepared updateCommand = getUpdate(session); |
456 | if (updateAction == CASCADE) { |
457 | ArrayList<Parameter> params = updateCommand.getParameters(); |
458 | for (int i = 0, len = columns.length; i < len; i++) { |
459 | Parameter param = params.get(i); |
460 | Column refCol = refColumns[i].column; |
461 | param.setValue(newRow.getValue(refCol.getColumnId())); |
462 | } |
463 | } |
464 | setWhere(updateCommand, columns.length, oldRow); |
465 | updateWithSkipCheck(updateCommand); |
466 | } |
467 | } |
468 | } |
469 | |
470 | private void updateWithSkipCheck(Prepared prep) { |
471 | // TODO constraints: maybe delay the update or support delayed checks |
472 | // (until commit) |
473 | try { |
474 | // TODO multithreaded kernel: this works only if nobody else updates |
475 | // this or the ref table at the same time |
476 | skipOwnTable = true; |
477 | prep.update(); |
478 | } finally { |
479 | skipOwnTable = false; |
480 | } |
481 | } |
482 | |
483 | private void setWhere(Prepared command, int pos, Row row) { |
484 | for (int i = 0, len = refColumns.length; i < len; i++) { |
485 | int idx = refColumns[i].column.getColumnId(); |
486 | Value v = row.getValue(idx); |
487 | ArrayList<Parameter> params = command.getParameters(); |
488 | Parameter param = params.get(pos + i); |
489 | param.setValue(v); |
490 | } |
491 | } |
492 | |
493 | public int getDeleteAction() { |
494 | return deleteAction; |
495 | } |
496 | |
497 | /** |
498 | * Set the action to apply (restrict, cascade,...) on a delete. |
499 | * |
500 | * @param action the action |
501 | */ |
502 | public void setDeleteAction(int action) { |
503 | if (action == deleteAction && deleteSQL == null) { |
504 | return; |
505 | } |
506 | if (deleteAction != RESTRICT) { |
507 | throw DbException.get(ErrorCode.CONSTRAINT_ALREADY_EXISTS_1, "ON DELETE"); |
508 | } |
509 | this.deleteAction = action; |
510 | buildDeleteSQL(); |
511 | } |
512 | |
513 | private void buildDeleteSQL() { |
514 | if (deleteAction == RESTRICT) { |
515 | return; |
516 | } |
517 | StatementBuilder buff = new StatementBuilder(); |
518 | if (deleteAction == CASCADE) { |
519 | buff.append("DELETE FROM ").append(table.getSQL()); |
520 | } else { |
521 | appendUpdate(buff); |
522 | } |
523 | appendWhere(buff); |
524 | deleteSQL = buff.toString(); |
525 | } |
526 | |
527 | private Prepared getUpdate(Session session) { |
528 | return prepare(session, updateSQL, updateAction); |
529 | } |
530 | |
531 | private Prepared getDelete(Session session) { |
532 | return prepare(session, deleteSQL, deleteAction); |
533 | } |
534 | |
535 | public int getUpdateAction() { |
536 | return updateAction; |
537 | } |
538 | |
539 | /** |
540 | * Set the action to apply (restrict, cascade,...) on an update. |
541 | * |
542 | * @param action the action |
543 | */ |
544 | public void setUpdateAction(int action) { |
545 | if (action == updateAction && updateSQL == null) { |
546 | return; |
547 | } |
548 | if (updateAction != RESTRICT) { |
549 | throw DbException.get(ErrorCode.CONSTRAINT_ALREADY_EXISTS_1, "ON UPDATE"); |
550 | } |
551 | this.updateAction = action; |
552 | buildUpdateSQL(); |
553 | } |
554 | |
555 | private void buildUpdateSQL() { |
556 | if (updateAction == RESTRICT) { |
557 | return; |
558 | } |
559 | StatementBuilder buff = new StatementBuilder(); |
560 | appendUpdate(buff); |
561 | appendWhere(buff); |
562 | updateSQL = buff.toString(); |
563 | } |
564 | |
565 | @Override |
566 | public void rebuild() { |
567 | buildUpdateSQL(); |
568 | buildDeleteSQL(); |
569 | } |
570 | |
571 | private Prepared prepare(Session session, String sql, int action) { |
572 | Prepared command = session.prepare(sql); |
573 | if (action != CASCADE) { |
574 | ArrayList<Parameter> params = command.getParameters(); |
575 | for (int i = 0, len = columns.length; i < len; i++) { |
576 | Column column = columns[i].column; |
577 | Parameter param = params.get(i); |
578 | Value value; |
579 | if (action == SET_NULL) { |
580 | value = ValueNull.INSTANCE; |
581 | } else { |
582 | Expression expr = column.getDefaultExpression(); |
583 | if (expr == null) { |
584 | throw DbException.get(ErrorCode.NO_DEFAULT_SET_1, column.getName()); |
585 | } |
586 | value = expr.getValue(session); |
587 | } |
588 | param.setValue(value); |
589 | } |
590 | } |
591 | return command; |
592 | } |
593 | |
594 | private void appendUpdate(StatementBuilder buff) { |
595 | buff.append("UPDATE ").append(table.getSQL()).append(" SET "); |
596 | buff.resetCount(); |
597 | for (IndexColumn c : columns) { |
598 | buff.appendExceptFirst(" , "); |
599 | buff.append(Parser.quoteIdentifier(c.column.getName())).append("=?"); |
600 | } |
601 | } |
602 | |
603 | private void appendWhere(StatementBuilder buff) { |
604 | buff.append(" WHERE "); |
605 | buff.resetCount(); |
606 | for (IndexColumn c : columns) { |
607 | buff.appendExceptFirst(" AND "); |
608 | buff.append(Parser.quoteIdentifier(c.column.getName())).append("=?"); |
609 | } |
610 | } |
611 | |
612 | @Override |
613 | public Table getRefTable() { |
614 | return refTable; |
615 | } |
616 | |
617 | @Override |
618 | public boolean usesIndex(Index idx) { |
619 | return idx == index || idx == refIndex; |
620 | } |
621 | |
622 | @Override |
623 | public void setIndexOwner(Index index) { |
624 | if (this.index == index) { |
625 | indexOwner = true; |
626 | } else if (this.refIndex == index) { |
627 | refIndexOwner = true; |
628 | } else { |
629 | DbException.throwInternalError(); |
630 | } |
631 | } |
632 | |
633 | @Override |
634 | public boolean isBefore() { |
635 | return false; |
636 | } |
637 | |
638 | @Override |
639 | public void checkExistingData(Session session) { |
640 | if (session.getDatabase().isStarting()) { |
641 | // don't check at startup |
642 | return; |
643 | } |
644 | session.startStatementWithinTransaction(); |
645 | StatementBuilder buff = new StatementBuilder("SELECT 1 FROM (SELECT "); |
646 | for (IndexColumn c : columns) { |
647 | buff.appendExceptFirst(", "); |
648 | buff.append(c.getSQL()); |
649 | } |
650 | buff.append(" FROM ").append(table.getSQL()).append(" WHERE "); |
651 | buff.resetCount(); |
652 | for (IndexColumn c : columns) { |
653 | buff.appendExceptFirst(" AND "); |
654 | buff.append(c.getSQL()).append(" IS NOT NULL "); |
655 | } |
656 | buff.append(" ORDER BY "); |
657 | buff.resetCount(); |
658 | for (IndexColumn c : columns) { |
659 | buff.appendExceptFirst(", "); |
660 | buff.append(c.getSQL()); |
661 | } |
662 | buff.append(") C WHERE NOT EXISTS(SELECT 1 FROM "). |
663 | append(refTable.getSQL()).append(" P WHERE "); |
664 | buff.resetCount(); |
665 | int i = 0; |
666 | for (IndexColumn c : columns) { |
667 | buff.appendExceptFirst(" AND "); |
668 | buff.append("C.").append(c.getSQL()).append('='). |
669 | append("P.").append(refColumns[i++].getSQL()); |
670 | } |
671 | buff.append(')'); |
672 | String sql = buff.toString(); |
673 | ResultInterface r = session.prepare(sql).query(1); |
674 | if (r.next()) { |
675 | throw DbException.get(ErrorCode.REFERENTIAL_INTEGRITY_VIOLATED_PARENT_MISSING_1, |
676 | getShortDescription(null, null)); |
677 | } |
678 | } |
679 | |
680 | @Override |
681 | public Index getUniqueIndex() { |
682 | return refIndex; |
683 | } |
684 | |
685 | } |