/** * Copyright (C) 2009-2015 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.store; import com.foundationdb.ais.model.ForeignKey; import com.foundationdb.ais.model.Index; import com.foundationdb.server.error.ForeignKeyReferencedViolationException; import com.foundationdb.server.error.ForeignKeyReferencingViolationException; import com.foundationdb.server.error.InvalidOperationException; import com.foundationdb.server.service.session.Session; import com.persistit.Key; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import static com.foundationdb.server.store.MemoryStore.BYTES_00; import static com.foundationdb.server.store.MemoryStore.BYTES_FF; import static com.foundationdb.server.store.MemoryStore.join; import static com.foundationdb.server.store.MemoryStore.packKey; import static com.foundationdb.server.store.MemoryStore.unpackKey; public class MemoryIndexChecks { public static enum CheckPass { ROW, STATEMENT, TRANSACTION, } public static class PendingChecks { private final List<IndexCheck> pending; public PendingChecks() { this.pending = new ArrayList<>(); } public void add(Session session, MemoryTransaction txn, IndexCheck check) { pending.add(check); performChecks(session, txn, CheckPass.ROW); } protected void performChecks(Session session, MemoryTransaction txn, CheckPass pass) { Iterator<IndexCheck> it = pending.iterator(); while(it.hasNext()) { IndexCheck check = it.next(); if(check.isReady(session, pass)) { check.query(session, txn); if(!check.check(session, pass)) { throw check.createException(session); } it.remove(); } } } public void clear() { pending.clear(); } } public static IndexCheck foreignKeyReferencingCheck(MemoryStoreData storeData, Index index, ForeignKey foreignKey, CheckPass finalPass, String operation) { byte[][] bounds = makeBounds(storeData, index); return new ForeignKeyReferencingCheck(index, bounds[0], bounds[1], foreignKey, finalPass, operation); } public static IndexCheck foreignKeyNotReferencedCheck(MemoryStoreData storeData, Index index, boolean isWholeIndex, ForeignKey foreignKey, boolean isSelfReference, CheckPass finalPass, String operation) { if(isWholeIndex) { byte[] begin = packKey(index); byte[] end = join(begin, BYTES_FF); return new ForeignKeyNotReferencedWholeCheck(index, begin, end, foreignKey, finalPass, operation); } byte[][] bounds = makeBounds(storeData, index); if(isSelfReference) { return new ForeignKeyNotReferencedSkipSelfCheck(index, bounds[0], bounds[1], foreignKey, finalPass, operation); } return new ForeignKeyNotReferencedCheck(index, bounds[0], bounds[1], foreignKey, finalPass, operation); } // // Internal // private static enum FoundValue { NONE, ONE, MULTIPLE, } public static abstract class IndexCheck { protected final Index index; protected final byte[] beginKey; protected final byte[] endKey; protected FoundValue foundValue; protected IndexCheck(Index index, byte[] beginKey, byte[] endKey) { this.index = index; this.beginKey = beginKey; this.endKey = endKey; } /** Perform the index check. */ public void query(Session session, MemoryTransaction txn) { if(endKey == null) { byte[] value = txn.get(beginKey); foundValue = (value == null) ? FoundValue.NONE : FoundValue.ONE; } else { Iterator<Entry<byte[], byte[]>> it = txn.getRange(beginKey, endKey); if(!it.hasNext()) { foundValue = FoundValue.NONE; } else { it.next(); foundValue = it.hasNext() ? FoundValue.MULTIPLE : FoundValue.ONE; } } } public boolean isReady(Session session, CheckPass pass) { return true; } /** Return <code>true</code> if the check passes. */ public abstract boolean check(Session session, CheckPass pass); /** Create appropriate exception for failed check. */ public abstract InvalidOperationException createException(Session session); } private static abstract class ForeignKeyCheck extends IndexCheck { protected final ForeignKey foreignKey; protected final String operation; protected final CheckPass finalPass; protected ForeignKeyCheck(Index index, byte[] beginKey, byte[] endKey, ForeignKey foreignKey, CheckPass finalPass, String operation) { super(index, beginKey, endKey); this.foreignKey = foreignKey; this.finalPass = finalPass; this.operation = operation; } public boolean isReady(Session session, CheckPass pass) { return (finalPass.ordinal() <= pass.ordinal()); } } private static class ForeignKeyReferencingCheck extends ForeignKeyCheck { public ForeignKeyReferencingCheck(Index index, byte[] beginKey, byte[] endKey, ForeignKey foreignKey, CheckPass finalPass, String operation) { super(index, beginKey, endKey, foreignKey, finalPass, operation); } @Override public boolean check(Session session, CheckPass pass) { assert foundValue != null; return foundValue != FoundValue.NONE; } @Override public InvalidOperationException createException(Session session) { Key persistitKey = new Key(null, 2047); unpackKey(index, beginKey, persistitKey); String key = ConstraintHandler.formatKey(session, index, persistitKey, foreignKey.getReferencingColumns(), foreignKey.getReferencedColumns()); return new ForeignKeyReferencingViolationException(operation, foreignKey.getReferencingTable().getName(), key, foreignKey.getConstraintName().getTableName(), foreignKey.getReferencedTable().getName()); } } private static class ForeignKeyNotReferencedCheck extends ForeignKeyCheck { public ForeignKeyNotReferencedCheck(Index index, byte[] beginKey, byte[] endKey, ForeignKey foreignKey, CheckPass finalPass, String operation) { super(index, beginKey, endKey, foreignKey, finalPass, operation); } @Override public boolean check(Session session, CheckPass pass) { assert foundValue != null; switch(foundValue) { case NONE: return true; case ONE: return isOneAllowed(pass); case MULTIPLE: return false; default: throw new IllegalStateException(foundValue.toString()); } } protected boolean isOneAllowed(CheckPass pass) { return false; } @Override public InvalidOperationException createException(Session session) { Key persistitKey = new Key(null, 2047); unpackKey(index, beginKey, persistitKey); String key = ConstraintHandler.formatKey(session, index, persistitKey, foreignKey.getReferencedColumns(), foreignKey.getReferencingColumns()); return new ForeignKeyReferencedViolationException(operation, foreignKey.getReferencedTable().getName(), key, foreignKey.getConstraintName().getTableName(), foreignKey.getReferencingTable().getName()); } } private static class ForeignKeyNotReferencedSkipSelfCheck extends ForeignKeyNotReferencedCheck { public ForeignKeyNotReferencedSkipSelfCheck(Index index, byte[] beginKey, byte[] endKey, ForeignKey foreignKey, CheckPass finalPass, String operation) { super(index, beginKey, endKey, foreignKey, finalPass, operation); } @Override public boolean isOneAllowed(CheckPass pass) { return (pass == CheckPass.ROW); } } static class ForeignKeyNotReferencedWholeCheck extends ForeignKeyCheck { private Key persistitKey; private Iterator<Entry<byte[],byte[]>> iterator; public ForeignKeyNotReferencedWholeCheck(Index index, byte[] beginKey, byte[] endKey, ForeignKey foreignKey, CheckPass finalPass, String operation) { super(index, beginKey, endKey, foreignKey, finalPass, operation); } private void ensureKey() { if(persistitKey == null) { persistitKey = new Key(null, 2047); } } @Override public void query(Session session, MemoryTransaction txn) { iterator = txn.getRange(beginKey, endKey); } @Override public boolean check(Session session, CheckPass pass) { while(iterator.hasNext()) { ensureKey(); byte[] rawKey = iterator.next().getKey(); unpackKey(index, rawKey, persistitKey); if(!ConstraintHandler.keyHasNullSegments(persistitKey, index)) { return false; } } return true; } @Override public InvalidOperationException createException(Session session) { // Should have been filled in failed check() assert persistitKey != null; String key = ConstraintHandler.formatKey(session, index, persistitKey, foreignKey.getReferencedColumns(), foreignKey.getReferencingColumns()); return new ForeignKeyReferencedViolationException(operation, foreignKey.getReferencedTable().getName(), key, foreignKey.getConstraintName().getTableName(), foreignKey.getReferencingTable().getName()); } } private static byte[][] makeBounds(MemoryStoreData storeData, Index index) { packKey(storeData); byte[] begin; byte[] end = null; // Normal case, reference does not contain all columns if(storeData.persistitKey.getDepth() < index.getAllColumns().size()) { begin = join(storeData.rawKey, BYTES_00); end = join(storeData.rawKey, BYTES_FF); } else { // Exactly matches index, including HKey columns begin = join(storeData.rawKey); } return new byte[][]{ begin, end }; } }