/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package db;

import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.util.*;

import org.junit.*;

import db.buffers.*;
import generic.test.AbstractGenericTest;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;

public class DBIndexedTableTest extends AbstractGenericTest {

	private static final int BUFFER_SIZE = 2048;// keep small for chained buffer testing
	private static final int CACHE_SIZE = 4 * 1024 * 1024;

	private static final int ITER_REC_CNT = 1000;

	private static final String table1Name = "TABLE1";

	private File testDir;
	private static final String dbName = "test";

	private BufferFileManager fileMgr;
	private DBHandle dbh;
	private BufferFile bfile;

	@Before
	public void setUp() throws Exception {

		testDir = createTempDirectory(getClass().getSimpleName());
		dbh = new DBHandle(BUFFER_SIZE, CACHE_SIZE);
	}

	@After
	public void tearDown() throws Exception {
		if (dbh != null) {
			dbh.close();
		}
		if (bfile != null) {
			bfile.close();
		}
		FileUtilities.deleteDir(testDir);

	}

	private void saveAsAndReopen(String name) throws IOException {
		try {
			BufferFileManager mgr = DBTestUtils.getBufferFileManager(testDir, name);
			BufferFile bf = new LocalManagedBufferFile(dbh.getBufferSize(), mgr, -1);
			dbh.saveAs(bf, true, null);
			dbh.close();
			fileMgr = mgr;
		}
		catch (CancelledException e) {
			Assert.fail("Should not happen");
		}
		bfile = new LocalManagedBufferFile(fileMgr, true, -1, -1);
		dbh = new DBHandle(bfile);
	}

	/**
	 * Insert the specified number of records using random keys.
	 * 
	 * @param table table instance, if null one will be created
	 * @param recordCnt number of records to insert.
	 * @param varDataSize size of variable length data fields.
	 * @return Record[] records which were inserted.
	 */
	private DBRecord[] createRandomTableRecords(int schemaType, int recordCnt, int varDataSize)
			throws IOException {
		long txId = dbh.startTransaction();
		Table table = DBTestUtils.createLongKeyTable(dbh, table1Name, schemaType, true, false);
		DBRecord[] recs = new DBRecord[recordCnt];
		for (int i = 0; i < recordCnt; i++) {
			try {
				recs[i] = DBTestUtils.createLongKeyRecord(table, true, varDataSize, true);
			}
			catch (DuplicateKeyException e) {
				Assert.fail("Duplicate key error");
			}
		}
		dbh.endTransaction(txId, true);
		return recs;
	}

	/**
	 * Insert the specified number of records using random keys.
	 * 
	 * @param recordCnt number of records to insert.
	 * @param varDataSize size of variable length data fields.
	 * @return Record[] records which were inserted.
	 */
	private DBRecord[] createOrderedTableRecords(int schemaType, int recordCnt, long keyIncrement,
			int varDataSize) throws IOException {
		long txId = dbh.startTransaction();
		Table table = DBTestUtils.createLongKeyTable(dbh, table1Name, schemaType, true, false);
		long key = 0;
		DBRecord[] recs = new DBRecord[recordCnt];
		for (int i = 0; i < recordCnt; i++) {
			try {
				recs[i] = DBTestUtils.createRecord(table, key, varDataSize, true);
			}
			catch (DuplicateKeyException e) {
				Assert.fail("Duplicate key error");
			}
			key += keyIncrement;
		}
		dbh.endTransaction(txId, true);
		return recs;
	}

	private Field[] matchingKeys(DBRecord[] recs, int columnIx, DBRecord matchRec) {
		ArrayList<DBRecord> recList = new ArrayList<>();
		Field f = matchRec.getField(columnIx);
		for (DBRecord rec : recs) {
			if (f.equals(rec.getField(columnIx))) {
				recList.add(rec);
			}
		}
		Field[] keys = new Field[recList.size()];
		Iterator<DBRecord> iter = recList.iterator();
		int i = 0;
		while (iter.hasNext()) {
			DBRecord rec = iter.next();
			keys[i++] = rec.getKeyField();
		}
		Arrays.sort(keys);
		return keys;
	}

	private void findRecords(boolean testStoredDB, int recordCnt, int findCnt, int varDataSize)
			throws IOException {
		DBRecord[] recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, recordCnt, varDataSize);// short var-len fields
		if (testStoredDB) {
			saveAsAndReopen(dbName);
		}
		Table table = dbh.getTable(table1Name);

		int step = recordCnt / findCnt;
		for (int indexColumn : table.getIndexedColumns()) {
			for (int i = 0; i < recordCnt; i += step) {
				Field[] keys = table.findRecords(recs[i].getField(indexColumn), indexColumn);
				Arrays.sort(keys);
				assertTrue(Arrays.equals(matchingKeys(recs, indexColumn, recs[i]), keys));
				assertEquals(keys.length,
					table.getMatchingRecordCount(recs[i].getField(indexColumn), indexColumn));
			}
		}
	}

	@Test
	public void testEmptyLongKeyIterator() throws IOException {
		createRandomTableRecords(DBTestUtils.ALL_TYPES, 0, 1);

		saveAsAndReopen(dbName);

		Table table = dbh.getTable(table1Name);
		assertEquals(0, table.getRecordCount());

		dbh.undo();
		dbh.redo();

		long startKey = 1500L;
		long minKey = 100L;
		long maxKey = 5000L;
		DBLongIterator iter = table.longKeyIterator();
		assertTrue(!iter.hasPrevious());
		assertTrue(!iter.hasNext());

		iter = table.longKeyIterator(startKey);
		assertTrue(!iter.hasPrevious());
		assertTrue(!iter.hasNext());

		iter = table.longKeyIterator(minKey, maxKey, startKey);
		assertTrue(!iter.hasPrevious());
		assertTrue(!iter.hasNext());

		startKey = -1L;
		iter = table.longKeyIterator(minKey, maxKey, startKey);
		assertTrue(!iter.hasPrevious());
		assertTrue(!iter.hasNext());

		startKey = 10000L;
		iter = table.longKeyIterator(minKey, maxKey, startKey);
		assertTrue(!iter.hasPrevious());
		assertTrue(!iter.hasNext());
	}

	@Test
	public void testFindRecordsSmallVLR() throws IOException {
		findRecords(false, 1000, 100, 8);
	}

	@Test
	public void testFindRecordsBigVLR() throws IOException {
		findRecords(false, 1000, 100, 16);
	}

	@Test
	public void testFindStoredRecordsSmallVLR() throws IOException {
		findRecords(true, 1000, 100, 8);
	}

	@Test
	public void testFindStoredRecordsEmptyVLR() throws IOException {
		findRecords(true, 1000, 100, 0);
	}

	@Test
	public void testFindStoredRecordsNullVLR() throws IOException {
		findRecords(true, 1000, 100, -1);
	}

	@Test
	public void testFindStoredRecordsBigVLR() throws IOException {
		findRecords(true, 1000, 100, 16);
	}

	private void updateRecordsIterator(boolean testStoredDB, int schemaType, int recordCnt,
			long keyIncrement, int varDataSize) throws IOException {
		DBRecord[] recs = null;
		if (keyIncrement == 0) {
			recs = createRandomTableRecords(schemaType, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(schemaType, recordCnt, keyIncrement, varDataSize);
		}
		if (testStoredDB) {
			saveAsAndReopen(dbName);
		}
		Table table = dbh.getTable(table1Name);

		// Find string and binary columns
		int strColumn = -1;
		int binColumn = -1;
		Field[] fields = table.getSchema().getFields();
		for (int i = 0; i < fields.length; i++) {
			if (!fields[i].isVariableLength()) {
				continue;
			}
			if (fields[i] instanceof StringField) {
				strColumn = i;
			}
			else if (fields[i] instanceof BinaryField) {
				binColumn = i;
			}
		}

		// Random update
		long txId = dbh.startTransaction();
		Random random = new Random(1);
		int updateCnt = recordCnt / 4;// update 1/4th of the records
		int varFldUpdate = 0;
		updateCnt = 206;
		for (int i = 0; i < updateCnt; i++) {
			int ran = random.nextInt();
			if (ran < 0) {
				ran = -ran;
			}
			int ix = ran % recordCnt;

			DBTestUtils.fillRecord(recs[ix], varDataSize);
			int r = varFldUpdate % 4;
			if ((r & 0x01) == 1 && strColumn >= 0) {
				// Update String field with null
				StringField sf = (StringField) recs[ix].getField(strColumn);
				sf.setString(null);
			}
			else if ((r & 0x02) == 2 && binColumn >= 0) {
				// Update binary field with null
				BinaryField bf = (BinaryField) recs[ix].getField(binColumn);
				bf.setBinaryData(null);
			}
			table.putRecord(recs[ix]);
			++varFldUpdate;
		}
		dbh.endTransaction(txId, true);

		assertEquals(recordCnt, table.getRecordCount());

		for (int colIx : table.getIndexedColumns()) {

			Arrays.sort(recs, new RecColumnComparator(colIx));

			// Forward iteration (no start)
			int recIx = 0;
			RecordIterator iter = table.indexIterator(colIx);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Forward iteration (start in middle - specify primary key)
			recIx = recordCnt / 2;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Reverse iteration (end - specify primary key)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Reverse iteration (start in middle - specify primary key)
			recIx = recordCnt / 2;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Forward iteration (start in middle)
			recIx = findStart(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Reverse iteration (end)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Reverse iteration (start in middle)
			recIx = findEnd(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			if (recs[0].getField(colIx) instanceof LongField) {

				// Forward iteration (start in middle - specify primary key)
				recIx = findStart(recs, recordCnt / 2, colIx);
				Field startValue = new LongField();
				startValue.setLongValue(recs[recIx].getField(colIx).getLongValue() - 1);
				iter = table.indexIteratorAfter(colIx, startValue);
				while (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
				assertEquals(recordCnt, recIx);

				// Reverse iteration (start in middle)
				recIx = findEnd(recs, recordCnt / 2, colIx);
				startValue.setLongValue(recs[recIx].getField(colIx).getLongValue() + 1);
				iter = table.indexIteratorBefore(colIx, startValue);
				while (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[recIx--], rec);
				}
				assertEquals(-1, recIx);

			}

		}

	}

	/**
	 * Test record iterator.
	 * 
	 * @param testStoredDB test against a stored database if true, else test against cached database
	 *            only.
	 * @param recordCnt number of records to test
	 * @param keyIncrement key increment, 0 = random
	 * @param varDataSize size of variable length data fields.
	 * @throws IOException
	 */
	private void iterateRecords(boolean testStoredDB, int schemaType, int recordCnt,
			long keyIncrement, int varDataSize, boolean doUndoRedo) throws IOException {

		DBRecord[] recs = null;
		if (keyIncrement == 0) {
			recs = createRandomTableRecords(schemaType, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(schemaType, recordCnt, keyIncrement, varDataSize);
		}

		if (testStoredDB) {
			saveAsAndReopen(dbName);
		}
		Table table = dbh.getTable(table1Name);
		assertEquals(recordCnt, table.getRecordCount());

		if (doUndoRedo) {
			dbh.undo();
			dbh.redo();
			table = dbh.getTable(table1Name);
			assertEquals(recordCnt, table.getRecordCount());
		}

		for (int colIx : table.getIndexedColumns()) {
			Arrays.sort(recs, new RecColumnComparator(colIx));

			int recIx;
			RecordIterator iter;
			ArrayList<Field> indexFields = new ArrayList<>();// list of unique index field values

			// Forward iteration (default iterator with no start) - also collect unique fields
			Field lastIndex = null;
			recIx = 0;
			iter = table.indexIterator(colIx);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx], rec);
				Field indexField = recs[recIx].getField(colIx);
				if (lastIndex == null || !lastIndex.equals(indexField)) {
					indexFields.add(indexField);
					lastIndex = indexField;
				}
				++recIx;
			}
			assertEquals(recordCnt, recIx);

			// Backward iteration (default iterator with no start)
			recIx = 0;
			iter = table.indexIterator(colIx);
			assertTrue(!iter.hasPrevious());

			// Forward iteration (after end)
			iter = table.indexIteratorAfter(colIx, recs[recordCnt - 1].getField(colIx));
			assertTrue(!iter.hasNext());

			// Backward iteration (before first)
			recIx = 0;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			assertTrue(!iter.hasPrevious());

			// Backward iteration (after first)
			int startIx = 0;
			recIx = findEnd(recs, startIx, colIx);
			iter = table.indexIteratorAfter(colIx, recs[startIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);
			assertTrue(!iter.hasPrevious());

			// Forward iteration (before first - specify primary key)
			startIx = 0;
			recIx = findStart(recs, startIx, colIx);
			iter = table.indexIteratorBefore(colIx, recs[startIx].getField(colIx),
				recs[startIx].getKeyField());
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Backward iteration (before first - specify primary key)
			startIx = 0;
			recIx = findStart(recs, startIx, colIx);
			iter = table.indexIteratorBefore(colIx, recs[startIx].getField(colIx),
				recs[startIx].getKeyField());
			assertTrue(!iter.hasPrevious());

			// Forward iteration (before first)
			recIx = 0;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Backward iteration (before first)
			recIx = 0;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			assertTrue(!iter.hasPrevious());

			// Forward iteration (end - specify primary key)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			assertTrue(!iter.hasNext());

			// Backward iteration (end - specify primary key)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Forward iteration (end)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			assertTrue(!iter.hasNext());

			// Backward iteration (end)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Forward iteration (start in middle - specify primary key)
			recIx = recordCnt / 2;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Backward iteration (start in middle - specify primary key)
			recIx = recordCnt / 2;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx),
				recs[recIx].getKeyField());
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Forward iteration (from start in middle)
			recIx = findStart(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Backward iteration (from start in middle)
			recIx = findStart(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[--recIx], rec);
			}
			assertEquals(0, recIx);

			// Forward iteration (from end in middle)
			recIx = findEnd(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[++recIx], rec);
			}
			assertEquals(recordCnt - 1, recIx);

			// Reverse iteration (from end in middle)
			recIx = findEnd(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Forward Range iterator
			int minIx = findStart(recs, recordCnt / 10, colIx);
			int maxIx = findEnd(recs, recordCnt / 5, colIx);
			recIx = minIx;
			iter = table.indexIterator(colIx, recs[minIx].getField(colIx),
				recs[maxIx].getField(colIx), true);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(maxIx + 1, recIx);

			// Full forward record iterator
			recIx = 0;
			iter = table.indexIterator(colIx, null, null, true);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Backward Range iterator
			minIx = findStart(recs, recordCnt / 10, colIx);
			maxIx = findEnd(recs, recordCnt / 5, colIx);
			recIx = maxIx;
			iter = table.indexIterator(colIx, recs[minIx].getField(colIx),
				recs[maxIx].getField(colIx), false);
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(minIx - 1, recIx);

			// Full reverse record iterator
			recIx = recordCnt - 1;
			iter = table.indexIterator(colIx, null, null, false);
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			if (recs[0].getField(colIx) instanceof LongField) {

				// Forward iteration (after end)
				long k = recs[recordCnt - 1].getField(colIx).getLongValue() + 1;
				iter = table.indexIteratorBefore(colIx, new LongField(k));
				assertTrue(!iter.hasNext());

				// Forward iteration (start in middle - specify primary key)
				recIx = findStart(recs, recordCnt / 2, colIx);
				Field startValue = new LongField();
				startValue.setLongValue(recs[recIx].getField(colIx).getLongValue() - 1);
				iter = table.indexIteratorAfter(colIx, startValue);
				while (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
				assertEquals(recordCnt, recIx);

				// Backward iteration (start in middle)
				recIx = findEnd(recs, recordCnt / 2, colIx);
				startValue.setLongValue(recs[recIx].getField(colIx).getLongValue() + 1);
				iter = table.indexIteratorBefore(colIx, startValue);
				while (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[recIx--], rec);
				}
				assertEquals(-1, recIx);
			}

			//*******************************************************************

			// Multi-direction check starting with forward iteration (start in middle)
			recIx = findStart(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			for (int i = 0; i < 2; i++) {
				if (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
			}
			--recIx;
			for (int i = 0; i < 2; i++) {
				if (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[recIx--], rec);
				}
			}
			++recIx;
			for (int i = 0; i < 2; i++) {
				if (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
			}
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			// Multi-direction check starting with backward iteration (start in middle)
			recIx = findStart(recs, recordCnt / 2, colIx);
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			--recIx;
			for (int i = 0; i < 2; i++) {
				if (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[recIx--], rec);
				}
			}
			++recIx;
			for (int i = 0; i < 2; i++) {
				if (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
			}
			--recIx;
			for (int i = 0; i < 2; i++) {
				if (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[recIx--], rec);
				}
			}
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Multi-direction check starting with forward iteration (start at End)
			recIx = recordCnt - 1;
			iter = table.indexIteratorAfter(colIx, recs[recIx].getField(colIx));
			for (int i = 0; i < 3; i++) {
				if (iter.hasNext()) {
					DBRecord rec = iter.next();
					assertEquals(recs[recIx++], rec);
				}
			}
			assertEquals(recordCnt - 1, recIx);
			while (iter.hasPrevious()) {
				DBRecord rec = iter.previous();
				assertEquals(recs[recIx--], rec);
			}
			assertEquals(-1, recIx);

			// Multi-direction check starting with backward iteration (start at Front)
			recIx = 0;
			iter = table.indexIteratorBefore(colIx, recs[recIx].getField(colIx));
			for (int i = 0; i < 3; i++) {
				if (iter.hasPrevious()) {
					DBRecord rec = iter.previous();
					assertEquals(recs[--recIx], rec);
				}
			}
			assertEquals(0, recIx);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(recordCnt, recIx);

			//
			// Index Field Iterator does not support variable length fields
			//
			if (lastIndex.isVariableLength()) {
				continue;// skip remain tests
			}

			// Index field iterator (all unique index values)
			minIx = 0;
			maxIx = indexFields.size() - 1;
			DBFieldIterator fiter = table.indexFieldIterator(colIx);
			int ix = minIx;
			while (fiter.hasNext()) {
				Field f = fiter.next();
				assertEquals(indexFields.get(ix++), f);
			}
			assertEquals(maxIx + 1, ix);

			// Index field iterator (forward range of unique index values)
			minIx = indexFields.size() / 10;
			maxIx = minIx * 2;
			fiter = table.indexFieldIterator(indexFields.get(minIx), indexFields.get(maxIx), true,
				colIx);
			ix = minIx;
			while (fiter.hasNext()) {
				Field f = fiter.next();
				assertEquals(indexFields.get(ix++), f);
			}
			assertEquals(maxIx + 1, ix);

			// Index field iterator (forward over all indexed fields)
			minIx = 0;
			maxIx = indexFields.size() - 1;
			fiter = table.indexFieldIterator(null, null, true, colIx);
			ix = minIx;
			assertTrue("Failed to position before min field", fiter.hasNext());
			while (fiter.hasNext()) {
				Field f = fiter.next();
				assertEquals(indexFields.get(ix++), f);
			}
			assertEquals(maxIx + 1, ix);

			// Index field iterator (reverse range of unique index values)
//			minIx = indexFields.size() / 10;
//			maxIx = minIx * 2;
			fiter = table.indexFieldIterator(indexFields.get(minIx), indexFields.get(maxIx), false,
				colIx);
			ix = maxIx;
			while (fiter.hasPrevious()) {
				Field f = fiter.previous();
				assertEquals(indexFields.get(ix--), f);
			}
			assertEquals(minIx - 1, ix);

			// Index field iterator (reverse over all indexed fields)
			minIx = 0;
			maxIx = indexFields.size() - 1;
			fiter = table.indexFieldIterator(null, null, false, colIx);
			ix = maxIx;
			assertTrue("Failed to position after max field", fiter.hasPrevious());
			while (fiter.hasPrevious()) {
				Field f = fiter.previous();
				assertEquals(indexFields.get(ix--), f);
			}
			assertEquals(-1, ix);

			// Index field iterator (forward range of unique index values)
//			minIx = indexFields.size() / 10;
//			maxIx = minIx * 2;
			startIx = (minIx + maxIx) / 2;
			fiter = table.indexFieldIterator(indexFields.get(minIx), indexFields.get(maxIx),
				indexFields.get(startIx), true, colIx);
			ix = startIx;
			while (fiter.hasNext()) {
				Field f = fiter.next();
				assertEquals(indexFields.get(ix++), f);
			}
			assertEquals(maxIx + 1, ix);

			// Index field iterator (reverse range of unique index values)
//			minIx = indexFields.size() / 10;
//			maxIx = minIx * 2;
//			startIx = (minIx + maxIx) / 2;
			fiter = table.indexFieldIterator(indexFields.get(minIx), indexFields.get(maxIx),
				indexFields.get(startIx), false, colIx);
			ix = startIx;
			while (fiter.hasPrevious()) {
				Field f = fiter.previous();
				assertEquals(indexFields.get(ix--), f);
			}
			assertEquals(minIx - 1, ix);
		}

	}

	/**
	 * Test record iterator.
	 * 
	 * @param testStoredDB test against a stored database if true, else test against cached database
	 *            only.
	 * @param recordCnt number of records to test
	 * @param keyIncrement key increment, 0 = random
	 * @param varDataSize size of variable length data fields.
	 * @throws IOException
	 */
	private void deleteIteratedRecords(int recordCnt, int testColIx, long keyIncrement,
			int varDataSize) throws IOException {

		DBRecord[] recs = null;
		if (keyIncrement == 0) {
			recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(DBTestUtils.ALL_TYPES, recordCnt, keyIncrement,
				varDataSize);
		}

		Table table = dbh.getTable(table1Name);

		Arrays.sort(recs, new RecColumnComparator(testColIx));

		// Forward - delete all records
		long txId = dbh.startTransaction();
		RecordIterator iter = table.indexIterator(testColIx);
		int recIx = 0;
		while (iter.hasNext()) {
			assertEquals(recs[recIx++], iter.next());
			assertTrue(iter.delete());
		}
		assertEquals(recIx, recs.length);

		dbh.deleteTable(table1Name);

		dbh.endTransaction(txId, true);

		if (keyIncrement == 0) {
			recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(DBTestUtils.ALL_TYPES, recordCnt, keyIncrement,
				varDataSize);
		}

		table = dbh.getTable(table1Name);

		Arrays.sort(recs, new RecColumnComparator(testColIx));

		// Reverse - delete all records
		txId = dbh.startTransaction();
		recIx = recs.length - 1;
		iter = table.indexIterator(testColIx, recs[0].getField(testColIx),
			recs[recIx].getField(testColIx), false);
		while (iter.hasPrevious()) {
			assertEquals(recs[recIx--], iter.previous());
			assertTrue(iter.delete());
		}
		assertEquals(recIx, -1);

		dbh.deleteTable(table1Name);

		dbh.endTransaction(txId, true);
	}

	/**
	 * Test record iterator.
	 * 
	 * @param testStoredDB test against a stored database if true, else test against cached database
	 *            only.
	 * @param recordCnt number of records to test
	 * @param keyIncrement key increment, 0 = random
	 * @param varDataSize size of variable length data fields.
	 * @throws IOException
	 */
	private void deleteIteratedIndexFields(int recordCnt, int testColIx, long keyIncrement,
			int varDataSize) throws Exception {

		DBRecord[] recs = null;
		if (keyIncrement == 0) {
			recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(DBTestUtils.ALL_TYPES, recordCnt, keyIncrement,
				varDataSize);
		}
		long txId = dbh.startTransaction();
		try {
			Table table = dbh.getTable(table1Name);

			Arrays.sort(recs, new RecColumnComparator(testColIx));

			// Count unique index values
			int fieldCnt = 0;
			Field lastField = null;
			ArrayList<Field> fieldList = new ArrayList<>();
			for (DBRecord rec : recs) {
				Field f = rec.getField(testColIx);
				if (lastField == null || !lastField.equals(f)) {
					lastField = f;
					fieldList.add(f);
					++fieldCnt;
				}
			}

			//
			// Index Field Iterator does not support variable length fields
			//
			if (lastField.isVariableLength()) {
				return;// skip test
			}

			// Forward - delete all records
			DBFieldIterator iter = table.indexFieldIterator(testColIx);
			int cnt = 0;
			while (iter.hasNext()) {
				assertEquals(fieldList.get(cnt++), iter.next());
				assertTrue(iter.delete());
			}
			assertEquals(fieldCnt, cnt);
			assertEquals(0, table.getRecordCount());
		}
		catch (Exception e) {
			e.printStackTrace();
			throw e;
		}
		finally {
			dbh.deleteTable(table1Name);
			dbh.endTransaction(txId, true);
		}

		if (keyIncrement == 0) {
			recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, recordCnt, varDataSize);
		}
		else {
			recs = createOrderedTableRecords(DBTestUtils.ALL_TYPES, recordCnt, keyIncrement,
				varDataSize);
		}
		txId = dbh.startTransaction();

		try {
			Table table = dbh.getTable(table1Name);

			Arrays.sort(recs, new RecColumnComparator(testColIx));

			// Count unique index values
			int fieldCnt = 0;
			Field lastField = null;
			ArrayList<Field> fieldList = new ArrayList<>();
			for (DBRecord rec : recs) {
				Field f = rec.getField(testColIx);
				if (lastField == null || !lastField.equals(f)) {
					lastField = f;
					fieldList.add(f);
					++fieldCnt;
				}
			}

			// Reverse - delete all records

			int cnt = fieldCnt - 1;
			int lastIx = recs.length - 1;
			DBFieldIterator iter = table.indexFieldIterator(recs[0].getField(testColIx),
				recs[lastIx].getField(testColIx), false, testColIx);
			while (iter.hasPrevious()) {
				assertEquals(fieldList.get(cnt--), iter.previous());
				assertTrue(iter.delete());
			}
			assertEquals(-1, cnt);
			assertEquals(0, table.getRecordCount());
		}
		finally {
			dbh.deleteTable(table1Name);
			dbh.endTransaction(txId, true);
		}
	}

	private int findStart(DBRecord[] recs, int startIx, int colIx) {
		Field f = recs[startIx].getField(colIx);
		--startIx;
		while (startIx >= 0 && f.equals(recs[startIx].getField(colIx))) {
			if (startIx == 0) {
				return startIx;
			}
			--startIx;
		}
		return ++startIx;
	}

	private int findEnd(DBRecord[] recs, int startIx, int colIx) {
		Field f = recs[startIx].getField(colIx);
		++startIx;
		while (startIx < recs.length && f.equals(recs[startIx].getField(colIx))) {
			if (startIx == recs.length) {
				return startIx;
			}
			++startIx;
		}
		return --startIx;
	}

	private class RecColumnComparator implements Comparator<DBRecord> {

		final int columnIx;

		RecColumnComparator(int columnIx) {
			this.columnIx = columnIx;
		}

		/**
		 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
		 */
		@Override
		public int compare(DBRecord rec1, DBRecord rec2) {
			int r = rec1.getField(columnIx).compareTo(rec2.getField(columnIx));
			if (r == 0) {
				if (rec1.getKey() == rec2.getKey()) {
					return 0;
				}
				else if (rec1.getKey() < rec2.getKey()) {
					return -1;
				}
				return 1;
			}
			return r;
		}
	}

	@Test
	public void testRandomRecordIterator() throws IOException {
		iterateRecords(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 0, -1, false);
	}

	@Test
	public void testForwardRecordIterator() throws IOException {
		iterateRecords(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 1, 1, false);
	}

	@Test
	public void testBackwardRecordIterator() throws IOException {
		iterateRecords(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, -1, 1, false);
	}

	@Test
	public void testStoredRandomRecordIterator() throws IOException {
		iterateRecords(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 0, 1, false);
	}

	@Test
	public void testStoredForwardRecordIterator() throws IOException {
		iterateRecords(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 1, 1, false);
	}

	@Test
	public void testStoredBackwardRecordIterator() throws IOException {
		iterateRecords(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, -1, 1, false);
	}

	@Test
	public void testRandomUpdateRecordIterator() throws IOException {
		updateRecordsIterator(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 0, 1);
	}

	@Test
	public void testForwardUpdateRecordIterator() throws IOException {
		updateRecordsIterator(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 1, 1);
	}

	@Test
	public void testBackwardUpdateRecordIterator() throws IOException {
		updateRecordsIterator(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, -1, 1);
	}

	@Test
	public void testStoredUpdateRandomRecordIterator() throws IOException {
		updateRecordsIterator(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 0, 1);
	}

	@Test
	public void testStoredUpdateForwardRecordIterator() throws IOException {
		updateRecordsIterator(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 1, 1);
	}

	@Test
	public void testStoredUpdateBackwardRecordIterator() throws IOException {
		updateRecordsIterator(true, DBTestUtils.ALL_TYPES, ITER_REC_CNT, -1, 1);
	}

	@Test
	public void testRandomRecordIteratorUndoRedo() throws IOException {
		iterateRecords(false, DBTestUtils.ALL_TYPES, ITER_REC_CNT, 0, -1, true);
	}

	@Test
	public void testRecordIteratorExtents() throws IOException {

		DBRecord[] recs = null;
		recs = createOrderedRecordRange(DBTestUtils.SINGLE_SHORT, 30, 2, 1);
		Table table = dbh.getTable(table1Name);
		assertEquals(recs.length, table.getRecordCount());

		int[] indexedColumns = table.getIndexedColumns();
		assertEquals(1, indexedColumns.length);

		// Backward Range iterator
		int colIx = 0;
		Arrays.sort(recs, new RecColumnComparator(colIx));
		int recIx = recs.length - 1;
//		RecordIterator iter = table.indexIterator(colIx, recs[minIx].getField(colIx),
//								   recs[maxIx].getField(colIx), false);
		Field minField = new ShortField(Short.MIN_VALUE);
		Field maxField = new ShortField(Short.MAX_VALUE);
		RecordIterator iter = table.indexIterator(colIx, minField, maxField, false);
		while (iter.hasPrevious()) {
			DBRecord rec = iter.previous();
			assertEquals(recs[recIx--], rec);
		}
		assertEquals(recIx, -1);
	}

	private DBRecord[] createOrderedRecordRange(int schemaType, int recordCnt, long keyIncrement,
			int varDataSize) throws IOException {
		long txId = dbh.startTransaction();
		Table table = DBTestUtils.createLongKeyTable(dbh, table1Name, schemaType, true, false);
		DBRecord[] recs = new DBRecord[recordCnt];
		for (int key = 0; key < recordCnt; key++) {
			try {
				recs[key] = DBTestUtils.createMidRangeRecord(table, key, varDataSize, true);
			}
			catch (DuplicateKeyException e) {
				Assert.fail("Duplicate key error");
			}
		}
		dbh.endTransaction(txId, true);
		return recs;
	}

	@Test
	public void testRecordIteratorDelete() throws IOException {
		for (int colIx : DBTestUtils.getIndexedColumns(DBTestUtils.ALL_TYPES)) {
			deleteIteratedRecords(ITER_REC_CNT, colIx, 1, 1);
		}
		for (int colIx : DBTestUtils.getIndexedColumns(DBTestUtils.ALL_TYPES)) {
			deleteIteratedRecords(ITER_REC_CNT, colIx, 0, 1);
		}
	}

	@Test
	public void testIndexFieldIteratorDelete() throws Exception {
		for (int colIx : DBTestUtils.getIndexedColumns(DBTestUtils.ALL_TYPES)) {
			deleteIteratedIndexFields(ITER_REC_CNT, colIx, 1, 1);
		}
		for (int colIx : DBTestUtils.getIndexedColumns(DBTestUtils.ALL_TYPES)) {
			deleteIteratedIndexFields(ITER_REC_CNT, colIx, 0, 1);
		}
	}

	@Test
	public void testConsistencyAndIndexRebuild() throws IOException {

		DBRecord[] recs = createRandomTableRecords(DBTestUtils.ALL_TYPES, ITER_REC_CNT, 10);

		long txId = dbh.startTransaction();
		try {
			assertTrue(dbh.isConsistent(TaskMonitor.DUMMY));
			dbh.rebuild(TaskMonitor.DUMMY);
		}
		catch (CancelledException e) {
			fail("unexpected cancel exception");
		}
		finally {
			dbh.endTransaction(txId, true);
		}

		Table table = dbh.getTable(table1Name);
		for (int colIx : table.getIndexedColumns()) {
			Arrays.sort(recs, new RecColumnComparator(colIx));
			int recIx = 0;
			RecordIterator iter = table.indexIterator(colIx);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(ITER_REC_CNT, recIx);
		}

		saveAsAndReopen(dbName);

		table = dbh.getTable(table1Name);
		for (int colIx : table.getIndexedColumns()) {
			Arrays.sort(recs, new RecColumnComparator(colIx));
			int recIx = 0;
			RecordIterator iter = table.indexIterator(colIx);
			while (iter.hasNext()) {
				DBRecord rec = iter.next();
				assertEquals(recs[recIx++], rec);
			}
			assertEquals(ITER_REC_CNT, recIx);
		}

	}

}
