EMMA Coverage Report (generated Sun Mar 01 22:06:14 CET 2015)
[all classes][org.h2.constraint]

COVERAGE SUMMARY FOR SOURCE FILE [ConstraintReferential.java]

nameclass, %method, %block, %line, %
ConstraintReferential.java100% (1/1)100% (43/43)97%  (1490/1540)96%  (320.9/334)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class ConstraintReferential100% (1/1)100% (43/43)97%  (1490/1540)96%  (320.9/334)
appendAction (StatementBuilder, int): void 100% (1/1)64%  (18/28)89%  (8/9)
setDeleteAction (int): void 100% (1/1)81%  (17/21)86%  (6/7)
setUpdateAction (int): void 100% (1/1)81%  (17/21)86%  (6/7)
setIndexOwner (Index): void 100% (1/1)89%  (17/19)83%  (5/6)
prepare (Session, String, int): Prepared 100% (1/1)91%  (52/57)93%  (13/14)
existsRow (Session, Index, SearchRow, Row): boolean 100% (1/1)94%  (72/77)81%  (17/21)
getCreateSQLForCopy (Table, Table, String, boolean): String 100% (1/1)94%  (176/188)94%  (34/36)
checkRowRefTable (Session, Row, Row): void 100% (1/1)95%  (97/102)96%  (23/24)
getShortDescription (Index, SearchRow): String 100% (1/1)99%  (137/139)100% (22.9/23)
checkExistingData (Session): void 100% (1/1)99%  (185/186)97%  (28/29)
ConstraintReferential (Schema, int, String, Table): void 100% (1/1)100% (7/7)100% (2/2)
appendUpdate (StatementBuilder): void 100% (1/1)100% (43/43)100% (6/6)
appendWhere (StatementBuilder): void 100% (1/1)100% (37/37)100% (6/6)
buildDeleteSQL (): void 100% (1/1)100% (32/32)100% (9/9)
buildUpdateSQL (): void 100% (1/1)100% (19/19)100% (7/7)
checkRow (Session, Row): void 100% (1/1)100% (72/72)100% (13/13)
checkRow (Session, Table, Row, Row): void 100% (1/1)100% (36/36)100% (10/10)
checkRowOwnTable (Session, Row, Row): void 100% (1/1)100% (165/165)100% (36/36)
getColumns (): IndexColumn [] 100% (1/1)100% (3/3)100% (1/1)
getConstraintType (): String 100% (1/1)100% (2/2)100% (1/1)
getCreateSQL (): String 100% (1/1)100% (7/7)100% (1/1)
getCreateSQLForCopy (Table, String): String 100% (1/1)100% (8/8)100% (1/1)
getCreateSQLWithoutIndexes (): String 100% (1/1)100% (10/10)100% (1/1)
getDelete (Session): Prepared 100% (1/1)100% (8/8)100% (1/1)
getDeleteAction (): int 100% (1/1)100% (3/3)100% (1/1)
getRefColumns (): IndexColumn [] 100% (1/1)100% (3/3)100% (1/1)
getRefTable (): Table 100% (1/1)100% (3/3)100% (1/1)
getReferencedColumns (Table): HashSet 100% (1/1)100% (57/57)100% (8/8)
getUniqueIndex (): Index 100% (1/1)100% (3/3)100% (1/1)
getUpdate (Session): Prepared 100% (1/1)100% (8/8)100% (1/1)
getUpdateAction (): int 100% (1/1)100% (3/3)100% (1/1)
isBefore (): boolean 100% (1/1)100% (2/2)100% (1/1)
isEqual (Row, Row): boolean 100% (1/1)100% (10/10)100% (1/1)
rebuild (): void 100% (1/1)100% (5/5)100% (3/3)
removeChildrenAndResources (Session): void 100% (1/1)100% (59/59)100% (17/17)
setColumns (IndexColumn []): void 100% (1/1)100% (4/4)100% (2/2)
setIndex (Index, boolean): void 100% (1/1)100% (7/7)100% (3/3)
setRefColumns (IndexColumn []): void 100% (1/1)100% (4/4)100% (2/2)
setRefIndex (Index, boolean): void 100% (1/1)100% (7/7)100% (3/3)
setRefTable (Table): void 100% (1/1)100% (10/10)100% (4/4)
setWhere (Prepared, int, Row): void 100% (1/1)100% (36/36)100% (7/7)
updateWithSkipCheck (Prepared): void 100% (1/1)100% (17/17)100% (5/5)
usesIndex (Index): boolean 100% (1/1)100% (12/12)100% (1/1)

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 */
6package org.h2.constraint;
7 
8import java.util.ArrayList;
9import java.util.HashSet;
10 
11import org.h2.api.ErrorCode;
12import org.h2.command.Parser;
13import org.h2.command.Prepared;
14import org.h2.engine.Session;
15import org.h2.expression.Expression;
16import org.h2.expression.Parameter;
17import org.h2.index.Cursor;
18import org.h2.index.Index;
19import org.h2.message.DbException;
20import org.h2.result.ResultInterface;
21import org.h2.result.Row;
22import org.h2.result.SearchRow;
23import org.h2.schema.Schema;
24import org.h2.table.Column;
25import org.h2.table.IndexColumn;
26import org.h2.table.Table;
27import org.h2.util.New;
28import org.h2.util.StatementBuilder;
29import org.h2.util.StringUtils;
30import org.h2.value.Value;
31import org.h2.value.ValueNull;
32 
33/**
34 * A referential constraint.
35 */
36public 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}

[all classes][org.h2.constraint]
EMMA 2.0.5312 (C) Vladimir Roubtsov