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 | */ |
6 | package org.h2.schema; |
7 | |
8 | import java.math.BigInteger; |
9 | |
10 | import org.h2.api.ErrorCode; |
11 | import org.h2.engine.DbObject; |
12 | import org.h2.engine.Session; |
13 | import org.h2.message.DbException; |
14 | import org.h2.message.Trace; |
15 | import org.h2.table.Table; |
16 | |
17 | /** |
18 | * A sequence is created using the statement |
19 | * CREATE SEQUENCE |
20 | */ |
21 | public class Sequence extends SchemaObjectBase { |
22 | |
23 | /** |
24 | * The default cache size for sequences. |
25 | */ |
26 | public static final int DEFAULT_CACHE_SIZE = 32; |
27 | |
28 | private long value; |
29 | private long valueWithMargin; |
30 | private long increment; |
31 | private long cacheSize; |
32 | private long minValue; |
33 | private long maxValue; |
34 | private boolean cycle; |
35 | private boolean belongsToTable; |
36 | |
37 | /** |
38 | * Creates a new sequence for an auto-increment column. |
39 | * |
40 | * @param schema the schema |
41 | * @param id the object id |
42 | * @param name the sequence name |
43 | * @param startValue the first value to return |
44 | * @param increment the increment count |
45 | */ |
46 | public Sequence(Schema schema, int id, String name, long startValue, |
47 | long increment) { |
48 | this(schema, id, name, startValue, increment, null, null, null, false, |
49 | true); |
50 | } |
51 | |
52 | /** |
53 | * Creates a new sequence. |
54 | * |
55 | * @param schema the schema |
56 | * @param id the object id |
57 | * @param name the sequence name |
58 | * @param startValue the first value to return |
59 | * @param increment the increment count |
60 | * @param cacheSize the number of entries to pre-fetch |
61 | * @param minValue the minimum value |
62 | * @param maxValue the maximum value |
63 | * @param cycle whether to jump back to the min value if needed |
64 | * @param belongsToTable whether this sequence belongs to a table (for |
65 | * auto-increment columns) |
66 | */ |
67 | public Sequence(Schema schema, int id, String name, Long startValue, |
68 | Long increment, Long cacheSize, Long minValue, Long maxValue, |
69 | boolean cycle, boolean belongsToTable) { |
70 | initSchemaObjectBase(schema, id, name, Trace.SEQUENCE); |
71 | this.increment = increment != null ? |
72 | increment : 1; |
73 | this.minValue = minValue != null ? |
74 | minValue : getDefaultMinValue(startValue, this.increment); |
75 | this.maxValue = maxValue != null ? |
76 | maxValue : getDefaultMaxValue(startValue, this.increment); |
77 | this.value = startValue != null ? |
78 | startValue : getDefaultStartValue(this.increment); |
79 | this.valueWithMargin = value; |
80 | this.cacheSize = cacheSize != null ? |
81 | Math.max(1, cacheSize) : DEFAULT_CACHE_SIZE; |
82 | this.cycle = cycle; |
83 | this.belongsToTable = belongsToTable; |
84 | if (!isValid(this.value, this.minValue, this.maxValue, this.increment)) { |
85 | throw DbException.get(ErrorCode.SEQUENCE_ATTRIBUTES_INVALID, name, |
86 | String.valueOf(this.value), String.valueOf(this.minValue), |
87 | String.valueOf(this.maxValue), |
88 | String.valueOf(this.increment)); |
89 | } |
90 | } |
91 | |
92 | /** |
93 | * Allows the start value, increment, min value and max value to be updated |
94 | * atomically, including atomic validation. Useful because setting these |
95 | * attributes one after the other could otherwise result in an invalid |
96 | * sequence state (e.g. min value > max value, start value < min value, |
97 | * etc). |
98 | * |
99 | * @param startValue the new start value (<code>null</code> if no change) |
100 | * @param minValue the new min value (<code>null</code> if no change) |
101 | * @param maxValue the new max value (<code>null</code> if no change) |
102 | * @param increment the new increment (<code>null</code> if no change) |
103 | */ |
104 | public synchronized void modify(Long startValue, Long minValue, |
105 | Long maxValue, Long increment) { |
106 | if (startValue == null) { |
107 | startValue = this.value; |
108 | } |
109 | if (minValue == null) { |
110 | minValue = this.minValue; |
111 | } |
112 | if (maxValue == null) { |
113 | maxValue = this.maxValue; |
114 | } |
115 | if (increment == null) { |
116 | increment = this.increment; |
117 | } |
118 | if (!isValid(startValue, minValue, maxValue, increment)) { |
119 | throw DbException.get(ErrorCode.SEQUENCE_ATTRIBUTES_INVALID, |
120 | getName(), String.valueOf(startValue), |
121 | String.valueOf(minValue), |
122 | String.valueOf(maxValue), |
123 | String.valueOf(increment)); |
124 | } |
125 | this.value = startValue; |
126 | this.valueWithMargin = startValue; |
127 | this.minValue = minValue; |
128 | this.maxValue = maxValue; |
129 | this.increment = increment; |
130 | } |
131 | |
132 | /** |
133 | * Validates the specified prospective start value, min value, max value and |
134 | * increment relative to each other, since each of their respective |
135 | * validities are contingent on the values of the other parameters. |
136 | * |
137 | * @param value the prospective start value |
138 | * @param minValue the prospective min value |
139 | * @param maxValue the prospective max value |
140 | * @param increment the prospective increment |
141 | */ |
142 | private static boolean isValid(long value, long minValue, long maxValue, |
143 | long increment) { |
144 | return minValue <= value && |
145 | maxValue >= value && |
146 | maxValue > minValue && |
147 | increment != 0 && |
148 | // Math.abs(increment) < maxValue - minValue |
149 | // use BigInteger to avoid overflows when maxValue and minValue |
150 | // are really big |
151 | BigInteger.valueOf(increment).abs().compareTo( |
152 | BigInteger.valueOf(maxValue).subtract(BigInteger.valueOf(minValue))) < 0; |
153 | } |
154 | |
155 | private static long getDefaultMinValue(Long startValue, long increment) { |
156 | long v = increment >= 0 ? 1 : Long.MIN_VALUE; |
157 | if (startValue != null && increment >= 0 && startValue < v) { |
158 | v = startValue; |
159 | } |
160 | return v; |
161 | } |
162 | |
163 | private static long getDefaultMaxValue(Long startValue, long increment) { |
164 | long v = increment >= 0 ? Long.MAX_VALUE : -1; |
165 | if (startValue != null && increment < 0 && startValue > v) { |
166 | v = startValue; |
167 | } |
168 | return v; |
169 | } |
170 | |
171 | private long getDefaultStartValue(long increment) { |
172 | return increment >= 0 ? minValue : maxValue; |
173 | } |
174 | |
175 | public boolean getBelongsToTable() { |
176 | return belongsToTable; |
177 | } |
178 | |
179 | public long getIncrement() { |
180 | return increment; |
181 | } |
182 | |
183 | public long getMinValue() { |
184 | return minValue; |
185 | } |
186 | |
187 | public long getMaxValue() { |
188 | return maxValue; |
189 | } |
190 | |
191 | public boolean getCycle() { |
192 | return cycle; |
193 | } |
194 | |
195 | public void setCycle(boolean cycle) { |
196 | this.cycle = cycle; |
197 | } |
198 | |
199 | @Override |
200 | public String getDropSQL() { |
201 | if (getBelongsToTable()) { |
202 | return null; |
203 | } |
204 | return "DROP SEQUENCE IF EXISTS " + getSQL(); |
205 | } |
206 | |
207 | @Override |
208 | public String getCreateSQLForCopy(Table table, String quotedName) { |
209 | throw DbException.throwInternalError(); |
210 | } |
211 | |
212 | @Override |
213 | public synchronized String getCreateSQL() { |
214 | StringBuilder buff = new StringBuilder("CREATE SEQUENCE "); |
215 | buff.append(getSQL()).append(" START WITH ").append(value); |
216 | if (increment != 1) { |
217 | buff.append(" INCREMENT BY ").append(increment); |
218 | } |
219 | if (minValue != getDefaultMinValue(value, increment)) { |
220 | buff.append(" MINVALUE ").append(minValue); |
221 | } |
222 | if (maxValue != getDefaultMaxValue(value, increment)) { |
223 | buff.append(" MAXVALUE ").append(maxValue); |
224 | } |
225 | if (cycle) { |
226 | buff.append(" CYCLE"); |
227 | } |
228 | if (cacheSize != DEFAULT_CACHE_SIZE) { |
229 | buff.append(" CACHE ").append(cacheSize); |
230 | } |
231 | if (belongsToTable) { |
232 | buff.append(" BELONGS_TO_TABLE"); |
233 | } |
234 | return buff.toString(); |
235 | } |
236 | |
237 | /** |
238 | * Get the next value for this sequence. |
239 | * |
240 | * @param session the session |
241 | * @return the next value |
242 | */ |
243 | public synchronized long getNext(Session session) { |
244 | boolean needsFlush = false; |
245 | if ((increment > 0 && value >= valueWithMargin) || |
246 | (increment < 0 && value <= valueWithMargin)) { |
247 | valueWithMargin += increment * cacheSize; |
248 | needsFlush = true; |
249 | } |
250 | if ((increment > 0 && value > maxValue) || |
251 | (increment < 0 && value < minValue)) { |
252 | if (cycle) { |
253 | value = increment > 0 ? minValue : maxValue; |
254 | valueWithMargin = value + (increment * cacheSize); |
255 | needsFlush = true; |
256 | } else { |
257 | throw DbException.get(ErrorCode.SEQUENCE_EXHAUSTED, getName()); |
258 | } |
259 | } |
260 | if (needsFlush) { |
261 | flush(session); |
262 | } |
263 | long v = value; |
264 | value += increment; |
265 | return v; |
266 | } |
267 | |
268 | /** |
269 | * Flush the current value to disk. |
270 | */ |
271 | public void flushWithoutMargin() { |
272 | if (valueWithMargin != value) { |
273 | valueWithMargin = value; |
274 | flush(null); |
275 | } |
276 | } |
277 | |
278 | /** |
279 | * Flush the current value, including the margin, to disk. |
280 | * |
281 | * @param session the session |
282 | */ |
283 | public synchronized void flush(Session session) { |
284 | if (session == null || !database.isSysTableLocked()) { |
285 | // This session may not lock the sys table (except if it already has |
286 | // locked it) because it must be committed immediately, otherwise |
287 | // other threads can not access the sys table. |
288 | Session sysSession = database.getSystemSession(); |
289 | synchronized (sysSession) { |
290 | flushInternal(sysSession); |
291 | sysSession.commit(false); |
292 | } |
293 | } else { |
294 | synchronized (session) { |
295 | flushInternal(session); |
296 | } |
297 | } |
298 | } |
299 | |
300 | private void flushInternal(Session session) { |
301 | // just for this case, use the value with the margin for the script |
302 | long realValue = value; |
303 | try { |
304 | value = valueWithMargin; |
305 | if (!isTemporary()) { |
306 | database.updateMeta(session, this); |
307 | } |
308 | } finally { |
309 | value = realValue; |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * Flush the current value to disk and close this object. |
315 | */ |
316 | public void close() { |
317 | flushWithoutMargin(); |
318 | } |
319 | |
320 | @Override |
321 | public int getType() { |
322 | return DbObject.SEQUENCE; |
323 | } |
324 | |
325 | @Override |
326 | public void removeChildrenAndResources(Session session) { |
327 | database.removeMeta(session, getId()); |
328 | invalidate(); |
329 | } |
330 | |
331 | @Override |
332 | public void checkRename() { |
333 | // nothing to do |
334 | } |
335 | |
336 | public synchronized long getCurrentValue() { |
337 | return value - increment; |
338 | } |
339 | |
340 | public void setBelongsToTable(boolean b) { |
341 | this.belongsToTable = b; |
342 | } |
343 | |
344 | public void setCacheSize(long cacheSize) { |
345 | this.cacheSize = Math.max(1, cacheSize); |
346 | } |
347 | |
348 | public long getCacheSize() { |
349 | return cacheSize; |
350 | } |
351 | |
352 | } |