/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.table;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import org.h2.api.DatabaseEventListener;
import org.h2.api.ErrorCode;
import org.h2.command.ddl.CreateTableData;
import org.h2.engine.Constants;
import org.h2.engine.DbObject;
import org.h2.engine.Session;
import org.h2.engine.SysProperties;
import org.h2.index.Cursor;
import org.h2.index.HashIndex;
import org.h2.index.Index;
import org.h2.index.IndexType;
import org.h2.index.NonUniqueHashIndex;
import org.h2.index.PageBtreeIndex;
import org.h2.index.PageDataIndex;
import org.h2.index.PageDelegateIndex;
import org.h2.index.ScanIndex;
import org.h2.index.SpatialTreeIndex;
import org.h2.index.TreeIndex;
import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.result.Row;
import org.h2.schema.SchemaObject;
import org.h2.util.MathUtils;
import org.h2.util.Utils;
import org.h2.value.CompareMode;

A table store in a PageStore.
/** * A table store in a PageStore. */
public class PageStoreTable extends RegularTable { private Index scanIndex; private long rowCount;
The queue of sessions waiting to lock the table. It is a FIFO queue to prevent starvation, since Java's synchronized locking is biased.
/** * The queue of sessions waiting to lock the table. It is a FIFO queue to * prevent starvation, since Java's synchronized locking is biased. */
private final ArrayDeque<Session> waitingSessions = new ArrayDeque<>(); private final Trace traceLock; private final ArrayList<Index> indexes = Utils.newSmallArrayList(); private long lastModificationId; private final PageDataIndex mainIndex; private int changesSinceAnalyze; private int nextAnalyze; public PageStoreTable(CreateTableData data) { super(data); nextAnalyze = database.getSettings().analyzeAuto; if (data.persistData && database.isPersistent()) { mainIndex = new PageDataIndex(this, data.id, IndexColumn.wrap(getColumns()), IndexType.createScan(data.persistData), data.create, data.session); scanIndex = mainIndex; } else { mainIndex = null; scanIndex = new ScanIndex(this, data.id, IndexColumn.wrap(getColumns()), IndexType.createScan(data.persistData)); } indexes.add(scanIndex); traceLock = database.getTrace(Trace.LOCK); } @Override public void close(Session session) { for (Index index : indexes) { index.close(session); } } @Override public Row getRow(Session session, long key) { return scanIndex.getRow(session, key); } @Override public void addRow(Session session, Row row) { lastModificationId = database.getNextModificationDataId(); int i = 0; try { for (int size = indexes.size(); i < size; i++) { Index index = indexes.get(i); index.add(session, row); checkRowCount(session, index, 1); } rowCount++; } catch (Throwable e) { try { while (--i >= 0) { Index index = indexes.get(i); index.remove(session, row); checkRowCount(session, index, 0); } } catch (DbException e2) { // this could happen, for example on failure in the storage // but if that is not the case it means there is something wrong // with the database trace.error(e2, "could not undo operation"); throw e2; } throw DbException.convert(e); } analyzeIfRequired(session); } private void checkRowCount(Session session, Index index, int offset) { if (SysProperties.CHECK) { if (!(index instanceof PageDelegateIndex)) { long rc = index.getRowCount(session); if (rc != rowCount + offset) { DbException.throwInternalError( "rowCount expected " + (rowCount + offset) + " got " + rc + " " + getName() + "." + index.getName()); } } } } @Override public Index getScanIndex(Session session) { return indexes.get(0); } @Override public Index getUniqueIndex() { for (Index idx : indexes) { if (idx.getIndexType().isUnique()) { return idx; } } return null; } @Override public ArrayList<Index> getIndexes() { return indexes; } @Override public Index addIndex(Session session, String indexName, int indexId, IndexColumn[] cols, IndexType indexType, boolean create, String indexComment) { if (indexType.isPrimaryKey()) { for (IndexColumn c : cols) { Column column = c.column; if (column.isNullable()) { throw DbException.get( ErrorCode.COLUMN_MUST_NOT_BE_NULLABLE_1, column.getName()); } column.setPrimaryKey(true); } } boolean isSessionTemporary = isTemporary() && !isGlobalTemporary(); if (!isSessionTemporary) { database.lockMeta(session); } Index index; if (isPersistIndexes() && indexType.isPersistent()) { int mainIndexColumn; if (database.isStarting() && database.getPageStore().getRootPageId(indexId) != 0) { mainIndexColumn = -1; } else if (!database.isStarting() && mainIndex.getRowCount(session) != 0 || mainIndex.getMainIndexColumn() != -1) { mainIndexColumn = -1; } else { mainIndexColumn = getMainIndexColumn(indexType, cols); } if (mainIndexColumn != -1) { mainIndex.setMainIndexColumn(mainIndexColumn); index = new PageDelegateIndex(this, indexId, indexName, indexType, mainIndex, create, session); } else if (indexType.isSpatial()) { index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType, true, create, session); } else { index = new PageBtreeIndex(this, indexId, indexName, cols, indexType, create, session); } } else { if (indexType.isHash()) { if (cols.length != 1) { throw DbException.getUnsupportedException( "hash indexes may index only one column"); } if (indexType.isUnique()) { index = new HashIndex(this, indexId, indexName, cols, indexType); } else { index = new NonUniqueHashIndex(this, indexId, indexName, cols, indexType); } } else if (indexType.isSpatial()) { index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType, false, true, session); } else { index = new TreeIndex(this, indexId, indexName, cols, indexType); } } if (index.needRebuild() && rowCount > 0) { try { Index scan = getScanIndex(session); long remaining = scan.getRowCount(session); long total = remaining; Cursor cursor = scan.find(session, null, null); long i = 0; int bufferSize = (int) Math.min(rowCount, database.getMaxMemoryRows()); ArrayList<Row> buffer = new ArrayList<>(bufferSize); String n = getName() + ":" + index.getName(); int t = MathUtils.convertLongToInt(total); while (cursor.next()) { database.setProgress(DatabaseEventListener.STATE_CREATE_INDEX, n, MathUtils.convertLongToInt(i++), t); Row row = cursor.get(); buffer.add(row); if (buffer.size() >= bufferSize) { addRowsToIndex(session, buffer, index); } remaining--; } addRowsToIndex(session, buffer, index); if (remaining != 0) { DbException.throwInternalError("rowcount remaining=" + remaining + " " + getName()); } } catch (DbException e) { getSchema().freeUniqueName(indexName); try { index.remove(session); } catch (DbException e2) { // this could happen, for example on failure in the storage // but if that is not the case it means // there is something wrong with the database trace.error(e2, "could not remove index"); throw e2; } throw e; } } index.setTemporary(isTemporary()); if (index.getCreateSQL() != null) { index.setComment(indexComment); if (isSessionTemporary) { session.addLocalTempTableIndex(index); } else { database.addSchemaObject(session, index); } } indexes.add(index); setModified(); return index; } @Override public long getRowCount(Session session) { return rowCount; } @Override public void removeRow(Session session, Row row) { lastModificationId = database.getNextModificationDataId(); int i = indexes.size() - 1; try { for (; i >= 0; i--) { Index index = indexes.get(i); index.remove(session, row); checkRowCount(session, index, -1); } rowCount--; } catch (Throwable e) { try { while (++i < indexes.size()) { Index index = indexes.get(i); index.add(session, row); checkRowCount(session, index, 0); } } catch (DbException e2) { // this could happen, for example on failure in the storage // but if that is not the case it means there is something wrong // with the database trace.error(e2, "could not undo operation"); throw e2; } throw DbException.convert(e); } analyzeIfRequired(session); } @Override public void truncate(Session session) { lastModificationId = database.getNextModificationDataId(); for (int i = indexes.size() - 1; i >= 0; i--) { Index index = indexes.get(i); index.truncate(session); } rowCount = 0; changesSinceAnalyze = 0; } private void analyzeIfRequired(Session session) { if (nextAnalyze == 0 || nextAnalyze > changesSinceAnalyze++) { return; } changesSinceAnalyze = 0; int n = 2 * nextAnalyze; if (n > 0) { nextAnalyze = n; } session.markTableForAnalyze(this); } @Override public boolean lock(Session session, boolean exclusive, boolean forceLockEvenInMvcc) { int lockMode = database.getLockMode(); if (lockMode == Constants.LOCK_MODE_OFF) { return lockExclusiveSession != null; } if (lockExclusiveSession == session) { return true; } if (!exclusive && lockSharedSessions.containsKey(session)) { return true; } synchronized (database) { if (!exclusive && lockSharedSessions.contains(session)) { return true; } session.setWaitForLock(this, Thread.currentThread()); waitingSessions.addLast(session); try { doLock1(session, lockMode, exclusive); } finally { session.setWaitForLock(null, null); waitingSessions.remove(session); } } return false; } private void doLock1(Session session, int lockMode, boolean exclusive) { traceLock(session, exclusive, "requesting for"); // don't get the current time unless necessary long max = 0; boolean checkDeadlock = false; while (true) { // if I'm the next one in the queue if (waitingSessions.getFirst() == session) { if (doLock2(session, lockMode, exclusive)) { return; } } if (checkDeadlock) { ArrayList<Session> sessions = checkDeadlock(session, null, null); if (sessions != null) { throw DbException.get(ErrorCode.DEADLOCK_1, getDeadlockDetails(sessions, exclusive)); } } else { // check for deadlocks from now on checkDeadlock = true; } long now = System.nanoTime(); if (max == 0) { // try at least one more time max = now + TimeUnit.MILLISECONDS.toNanos(session.getLockTimeout()); } else if (now >= max) { traceLock(session, exclusive, "timeout after " + session.getLockTimeout()); throw DbException.get(ErrorCode.LOCK_TIMEOUT_1, getName()); } try { traceLock(session, exclusive, "waiting for"); if (database.getLockMode() == Constants.LOCK_MODE_TABLE_GC) { for (int i = 0; i < 20; i++) { long free = Runtime.getRuntime().freeMemory(); System.gc(); long free2 = Runtime.getRuntime().freeMemory(); if (free == free2) { break; } } } // don't wait too long so that deadlocks are detected early long sleep = Math.min(Constants.DEADLOCK_CHECK, TimeUnit.NANOSECONDS.toMillis(max - now)); if (sleep == 0) { sleep = 1; } database.wait(sleep); } catch (InterruptedException e) { // ignore } } } private boolean doLock2(Session session, int lockMode, boolean exclusive) { if (exclusive) { if (lockExclusiveSession == null) { if (lockSharedSessions.isEmpty()) { traceLock(session, exclusive, "added for"); session.addLock(this); lockExclusiveSession = session; return true; } else if (lockSharedSessions.size() == 1 && lockSharedSessions.containsKey(session)) { traceLock(session, exclusive, "add (upgraded) for "); lockExclusiveSession = session; return true; } } } else { if (lockExclusiveSession == null) { if (lockMode == Constants.LOCK_MODE_READ_COMMITTED) { if (!database.isMultiThreaded()) { // READ_COMMITTED: a read lock is acquired, // but released immediately after the operation // is complete. // When allowing only one thread, no lock is // required. // Row level locks work like read committed. return true; } } if (!lockSharedSessions.containsKey(session)) { traceLock(session, exclusive, "ok"); session.addLock(this); lockSharedSessions.put(session, session); } return true; } } return false; } private void traceLock(Session session, boolean exclusive, String s) { if (traceLock.isDebugEnabled()) { traceLock.debug("{0} {1} {2} {3}", session.getId(), exclusive ? "exclusive write lock" : "shared read lock", s, getName()); } } @Override public void unlock(Session s) { if (database != null) { traceLock(s, lockExclusiveSession == s, "unlock"); if (lockExclusiveSession == s) { lockSharedSessions.remove(s); lockExclusiveSession = null; } synchronized (database) { if (!lockSharedSessions.isEmpty()) { lockSharedSessions.remove(s); } if (!waitingSessions.isEmpty()) { database.notifyAll(); } } } }
Set the row count of this table.
Params:
  • count – the row count
/** * Set the row count of this table. * * @param count the row count */
public void setRowCount(long count) { this.rowCount = count; } @Override public void removeChildrenAndResources(Session session) { if (containsLargeObject) { // unfortunately, the data is gone on rollback truncate(session); database.getLobStorage().removeAllForTable(getId()); database.lockMeta(session); } super.removeChildrenAndResources(session); // go backwards because database.removeIndex will call table.removeIndex while (indexes.size() > 1) { Index index = indexes.get(1); if (index.getName() != null) { database.removeSchemaObject(session, index); } // needed for session temporary indexes indexes.remove(index); } if (SysProperties.CHECK) { for (SchemaObject obj : database.getAllSchemaObjects(DbObject.INDEX)) { Index index = (Index) obj; if (index.getTable() == this) { DbException.throwInternalError("index not dropped: " + index.getName()); } } } scanIndex.remove(session); database.removeMeta(session, getId()); scanIndex = null; lockExclusiveSession = null; lockSharedSessions.clear(); invalidate(); } @Override public long getMaxDataModificationId() { return lastModificationId; } @Override public long getRowCountApproximation() { return scanIndex.getRowCountApproximation(); } @Override public long getDiskSpaceUsed() { return scanIndex.getDiskSpaceUsed(); } public void setCompareMode(CompareMode compareMode) { this.compareMode = compareMode; } }