/* * Copyright 2004-2014 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.schema; import java.math.BigInteger; import org.h2.api.ErrorCode; import org.h2.engine.DbObject; import org.h2.engine.Session; import org.h2.message.DbException; import org.h2.message.Trace; import org.h2.table.Table; /** * A sequence is created using the statement * CREATE SEQUENCE */ public class Sequence extends SchemaObjectBase { /** * The default cache size for sequences. */ public static final int DEFAULT_CACHE_SIZE = 32; private long value; private long valueWithMargin; private long increment; private long cacheSize; private long minValue; private long maxValue; private boolean cycle; private boolean belongsToTable; private Object flushSync = new Object(); private boolean writeWithMargin; /** * Creates a new sequence for an auto-increment column. * * @param schema the schema * @param id the object id * @param name the sequence name * @param startValue the first value to return * @param increment the increment count */ public Sequence(Schema schema, int id, String name, long startValue, long increment) { this(schema, id, name, startValue, increment, null, null, null, false, true); } /** * Creates a new sequence. * * @param schema the schema * @param id the object id * @param name the sequence name * @param startValue the first value to return * @param increment the increment count * @param cacheSize the number of entries to pre-fetch * @param minValue the minimum value * @param maxValue the maximum value * @param cycle whether to jump back to the min value if needed * @param belongsToTable whether this sequence belongs to a table (for * auto-increment columns) */ public Sequence(Schema schema, int id, String name, Long startValue, Long increment, Long cacheSize, Long minValue, Long maxValue, boolean cycle, boolean belongsToTable) { initSchemaObjectBase(schema, id, name, Trace.SEQUENCE); this.increment = increment != null ? increment : 1; this.minValue = minValue != null ? minValue : getDefaultMinValue(startValue, this.increment); this.maxValue = maxValue != null ? maxValue : getDefaultMaxValue(startValue, this.increment); this.value = startValue != null ? startValue : getDefaultStartValue(this.increment); this.valueWithMargin = value; this.cacheSize = cacheSize != null ? Math.max(1, cacheSize) : DEFAULT_CACHE_SIZE; this.cycle = cycle; this.belongsToTable = belongsToTable; if (!isValid(this.value, this.minValue, this.maxValue, this.increment)) { throw DbException.get(ErrorCode.SEQUENCE_ATTRIBUTES_INVALID, name, String.valueOf(this.value), String.valueOf(this.minValue), String.valueOf(this.maxValue), String.valueOf(this.increment)); } } /** * Allows the start value, increment, min value and max value to be updated * atomically, including atomic validation. Useful because setting these * attributes one after the other could otherwise result in an invalid * sequence state (e.g. min value > max value, start value < min value, * etc). * * @param startValue the new start value (<code>null</code> if no change) * @param minValue the new min value (<code>null</code> if no change) * @param maxValue the new max value (<code>null</code> if no change) * @param increment the new increment (<code>null</code> if no change) */ public synchronized void modify(Long startValue, Long minValue, Long maxValue, Long increment) { if (startValue == null) { startValue = this.value; } if (minValue == null) { minValue = this.minValue; } if (maxValue == null) { maxValue = this.maxValue; } if (increment == null) { increment = this.increment; } if (!isValid(startValue, minValue, maxValue, increment)) { throw DbException.get(ErrorCode.SEQUENCE_ATTRIBUTES_INVALID, getName(), String.valueOf(startValue), String.valueOf(minValue), String.valueOf(maxValue), String.valueOf(increment)); } this.value = startValue; this.valueWithMargin = startValue; this.minValue = minValue; this.maxValue = maxValue; this.increment = increment; } /** * Validates the specified prospective start value, min value, max value and * increment relative to each other, since each of their respective * validities are contingent on the values of the other parameters. * * @param value the prospective start value * @param minValue the prospective min value * @param maxValue the prospective max value * @param increment the prospective increment */ private static boolean isValid(long value, long minValue, long maxValue, long increment) { return minValue <= value && maxValue >= value && maxValue > minValue && increment != 0 && // Math.abs(increment) < maxValue - minValue // use BigInteger to avoid overflows when maxValue and minValue // are really big BigInteger.valueOf(increment).abs().compareTo( BigInteger.valueOf(maxValue).subtract(BigInteger.valueOf(minValue))) < 0; } private static long getDefaultMinValue(Long startValue, long increment) { long v = increment >= 0 ? 1 : Long.MIN_VALUE; if (startValue != null && increment >= 0 && startValue < v) { v = startValue; } return v; } private static long getDefaultMaxValue(Long startValue, long increment) { long v = increment >= 0 ? Long.MAX_VALUE : -1; if (startValue != null && increment < 0 && startValue > v) { v = startValue; } return v; } private long getDefaultStartValue(long increment) { return increment >= 0 ? minValue : maxValue; } public boolean getBelongsToTable() { return belongsToTable; } public long getIncrement() { return increment; } public long getMinValue() { return minValue; } public long getMaxValue() { return maxValue; } public boolean getCycle() { return cycle; } public void setCycle(boolean cycle) { this.cycle = cycle; } @Override public String getDropSQL() { if (getBelongsToTable()) { return null; } return "DROP SEQUENCE IF EXISTS " + getSQL(); } @Override public String getCreateSQLForCopy(Table table, String quotedName) { throw DbException.throwInternalError(toString()); } @Override public synchronized String getCreateSQL() { long v = writeWithMargin ? valueWithMargin : value; StringBuilder buff = new StringBuilder("CREATE SEQUENCE "); buff.append(getSQL()).append(" START WITH ").append(v); if (increment != 1) { buff.append(" INCREMENT BY ").append(increment); } if (minValue != getDefaultMinValue(v, increment)) { buff.append(" MINVALUE ").append(minValue); } if (maxValue != getDefaultMaxValue(v, increment)) { buff.append(" MAXVALUE ").append(maxValue); } if (cycle) { buff.append(" CYCLE"); } if (cacheSize != DEFAULT_CACHE_SIZE) { buff.append(" CACHE ").append(cacheSize); } if (belongsToTable) { buff.append(" BELONGS_TO_TABLE"); } return buff.toString(); } /** * Get the next value for this sequence. * * @param session the session * @return the next value */ public long getNext(Session session) { boolean needsFlush = false; long result; synchronized (this) { if ((increment > 0 && value >= valueWithMargin) || (increment < 0 && value <= valueWithMargin)) { valueWithMargin += increment * cacheSize; needsFlush = true; } if ((increment > 0 && value > maxValue) || (increment < 0 && value < minValue)) { if (cycle) { value = increment > 0 ? minValue : maxValue; valueWithMargin = value + (increment * cacheSize); needsFlush = true; } else { throw DbException.get(ErrorCode.SEQUENCE_EXHAUSTED, getName()); } } result = value; value += increment; } if (needsFlush) { flush(session); } return result; } /** * Flush the current value to disk. */ public void flushWithoutMargin() { if (valueWithMargin != value) { valueWithMargin = value; flush(null); } } /** * Flush the current value, including the margin, to disk. * * @param session the session */ public void flush(Session session) { if (isTemporary()) { return; } if (session == null || !database.isSysTableLockedBy(session)) { // This session may not lock the sys table (except if it has already // locked it) because it must be committed immediately, otherwise // other threads can not access the sys table. Session sysSession = database.getSystemSession(); synchronized (sysSession) { synchronized (flushSync) { flushInternal(sysSession); } sysSession.commit(false); } } else { synchronized (session) { synchronized (flushSync) { flushInternal(session); } } } } private void flushInternal(Session session) { final boolean metaWasLocked = database.lockMeta(session); // just for this case, use the value with the margin try { writeWithMargin = true; database.updateMeta(session, this); } finally { writeWithMargin = false; } if (!metaWasLocked) { database.unlockMeta(session); } } /** * Flush the current value to disk and close this object. */ public void close() { flushWithoutMargin(); } @Override public int getType() { return DbObject.SEQUENCE; } @Override public void removeChildrenAndResources(Session session) { database.removeMeta(session, getId()); invalidate(); } @Override public void checkRename() { // nothing to do } public synchronized long getCurrentValue() { return value - increment; } public void setBelongsToTable(boolean b) { this.belongsToTable = b; } public void setCacheSize(long cacheSize) { this.cacheSize = Math.max(1, cacheSize); } public long getCacheSize() { return cacheSize; } }