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.mvstore.db; |
7 | |
8 | import java.util.Arrays; |
9 | import java.util.Collections; |
10 | import java.util.Iterator; |
11 | import java.util.List; |
12 | import java.util.Map.Entry; |
13 | |
14 | import org.h2.api.ErrorCode; |
15 | import org.h2.engine.Constants; |
16 | import org.h2.engine.Database; |
17 | import org.h2.engine.Session; |
18 | import org.h2.index.BaseIndex; |
19 | import org.h2.index.Cursor; |
20 | import org.h2.index.IndexType; |
21 | import org.h2.message.DbException; |
22 | import org.h2.mvstore.DataUtils; |
23 | import org.h2.mvstore.db.TransactionStore.Transaction; |
24 | import org.h2.mvstore.db.TransactionStore.TransactionMap; |
25 | import org.h2.result.Row; |
26 | import org.h2.result.SearchRow; |
27 | import org.h2.result.SortOrder; |
28 | import org.h2.table.Column; |
29 | import org.h2.table.IndexColumn; |
30 | import org.h2.table.TableFilter; |
31 | import org.h2.value.Value; |
32 | import org.h2.value.ValueArray; |
33 | import org.h2.value.ValueLong; |
34 | import org.h2.value.ValueNull; |
35 | |
36 | /** |
37 | * A table stored in a MVStore. |
38 | */ |
39 | public class MVPrimaryIndex extends BaseIndex { |
40 | |
41 | /** |
42 | * The minimum long value. |
43 | */ |
44 | static final ValueLong MIN = ValueLong.get(Long.MIN_VALUE); |
45 | |
46 | /** |
47 | * The maximum long value. |
48 | */ |
49 | static final ValueLong MAX = ValueLong.get(Long.MAX_VALUE); |
50 | |
51 | /** |
52 | * The zero long value. |
53 | */ |
54 | static final ValueLong ZERO = ValueLong.get(0); |
55 | |
56 | private final MVTable mvTable; |
57 | private final String mapName; |
58 | private TransactionMap<Value, Value> dataMap; |
59 | private long lastKey; |
60 | private int mainIndexColumn = -1; |
61 | |
62 | public MVPrimaryIndex(Database db, MVTable table, int id, |
63 | IndexColumn[] columns, IndexType indexType) { |
64 | this.mvTable = table; |
65 | initBaseIndex(table, id, table.getName() + "_DATA", columns, indexType); |
66 | int[] sortTypes = new int[columns.length]; |
67 | for (int i = 0; i < columns.length; i++) { |
68 | sortTypes[i] = SortOrder.ASCENDING; |
69 | } |
70 | ValueDataType keyType = new ValueDataType(null, null, null); |
71 | ValueDataType valueType = new ValueDataType(db.getCompareMode(), db, |
72 | sortTypes); |
73 | mapName = "table." + getId(); |
74 | dataMap = mvTable.getTransaction(null).openMap(mapName, keyType, |
75 | valueType); |
76 | if (!table.isPersistData()) { |
77 | dataMap.map.setVolatile(true); |
78 | } |
79 | Value k = dataMap.lastKey(); |
80 | lastKey = k == null ? 0 : k.getLong(); |
81 | } |
82 | |
83 | @Override |
84 | public String getCreateSQL() { |
85 | return null; |
86 | } |
87 | |
88 | @Override |
89 | public String getPlanSQL() { |
90 | return table.getSQL() + ".tableScan"; |
91 | } |
92 | |
93 | public void setMainIndexColumn(int mainIndexColumn) { |
94 | this.mainIndexColumn = mainIndexColumn; |
95 | } |
96 | |
97 | public int getMainIndexColumn() { |
98 | return mainIndexColumn; |
99 | } |
100 | |
101 | @Override |
102 | public void close(Session session) { |
103 | // ok |
104 | } |
105 | |
106 | @Override |
107 | public void add(Session session, Row row) { |
108 | if (mainIndexColumn == -1) { |
109 | if (row.getKey() == 0) { |
110 | row.setKey(++lastKey); |
111 | } |
112 | } else { |
113 | long c = row.getValue(mainIndexColumn).getLong(); |
114 | row.setKey(c); |
115 | } |
116 | |
117 | if (mvTable.getContainsLargeObject()) { |
118 | for (int i = 0, len = row.getColumnCount(); i < len; i++) { |
119 | Value v = row.getValue(i); |
120 | Value v2 = v.link(database, getId()); |
121 | if (v2.isLinked()) { |
122 | session.unlinkAtCommitStop(v2); |
123 | } |
124 | if (v != v2) { |
125 | row.setValue(i, v2); |
126 | } |
127 | } |
128 | } |
129 | |
130 | TransactionMap<Value, Value> map = getMap(session); |
131 | Value key = ValueLong.get(row.getKey()); |
132 | Value old = map.getLatest(key); |
133 | if (old != null) { |
134 | String sql = "PRIMARY KEY ON " + table.getSQL(); |
135 | if (mainIndexColumn >= 0 && mainIndexColumn < indexColumns.length) { |
136 | sql += "(" + indexColumns[mainIndexColumn].getSQL() + ")"; |
137 | } |
138 | DbException e = DbException.get(ErrorCode.DUPLICATE_KEY_1, sql); |
139 | e.setSource(this); |
140 | throw e; |
141 | } |
142 | try { |
143 | map.put(key, ValueArray.get(row.getValueList())); |
144 | } catch (IllegalStateException e) { |
145 | throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, |
146 | e, table.getName()); |
147 | } |
148 | lastKey = Math.max(lastKey, row.getKey()); |
149 | } |
150 | |
151 | @Override |
152 | public void remove(Session session, Row row) { |
153 | if (mvTable.getContainsLargeObject()) { |
154 | for (int i = 0, len = row.getColumnCount(); i < len; i++) { |
155 | Value v = row.getValue(i); |
156 | if (v.isLinked()) { |
157 | session.unlinkAtCommit(v); |
158 | } |
159 | } |
160 | } |
161 | TransactionMap<Value, Value> map = getMap(session); |
162 | try { |
163 | Value old = map.remove(ValueLong.get(row.getKey())); |
164 | if (old == null) { |
165 | throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1, |
166 | getSQL() + ": " + row.getKey()); |
167 | } |
168 | } catch (IllegalStateException e) { |
169 | throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, |
170 | e, table.getName()); |
171 | } |
172 | } |
173 | |
174 | @Override |
175 | public Cursor find(Session session, SearchRow first, SearchRow last) { |
176 | ValueLong min, max; |
177 | if (first == null) { |
178 | min = MIN; |
179 | } else if (mainIndexColumn < 0) { |
180 | min = ValueLong.get(first.getKey()); |
181 | } else { |
182 | ValueLong v = (ValueLong) first.getValue(mainIndexColumn); |
183 | if (v == null) { |
184 | min = ValueLong.get(first.getKey()); |
185 | } else { |
186 | min = v; |
187 | } |
188 | } |
189 | if (last == null) { |
190 | max = MAX; |
191 | } else if (mainIndexColumn < 0) { |
192 | max = ValueLong.get(last.getKey()); |
193 | } else { |
194 | ValueLong v = (ValueLong) last.getValue(mainIndexColumn); |
195 | if (v == null) { |
196 | max = ValueLong.get(last.getKey()); |
197 | } else { |
198 | max = v; |
199 | } |
200 | } |
201 | TransactionMap<Value, Value> map = getMap(session); |
202 | return new MVStoreCursor(map.entryIterator(min), max); |
203 | } |
204 | |
205 | @Override |
206 | public MVTable getTable() { |
207 | return mvTable; |
208 | } |
209 | |
210 | @Override |
211 | public Row getRow(Session session, long key) { |
212 | TransactionMap<Value, Value> map = getMap(session); |
213 | Value v = map.get(ValueLong.get(key)); |
214 | ValueArray array = (ValueArray) v; |
215 | Row row = new Row(array.getList(), 0); |
216 | row.setKey(key); |
217 | return row; |
218 | } |
219 | |
220 | @Override |
221 | public double getCost(Session session, int[] masks, TableFilter filter, |
222 | SortOrder sortOrder) { |
223 | try { |
224 | long cost = 10 * (dataMap.sizeAsLongMax() + Constants.COST_ROW_OFFSET); |
225 | return cost; |
226 | } catch (IllegalStateException e) { |
227 | throw DbException.get(ErrorCode.OBJECT_CLOSED, e); |
228 | } |
229 | } |
230 | |
231 | @Override |
232 | public int getColumnIndex(Column col) { |
233 | // can not use this index - use the delegate index instead |
234 | return -1; |
235 | } |
236 | |
237 | @Override |
238 | public void remove(Session session) { |
239 | TransactionMap<Value, Value> map = getMap(session); |
240 | if (!map.isClosed()) { |
241 | Transaction t = mvTable.getTransaction(session); |
242 | t.removeMap(map); |
243 | } |
244 | } |
245 | |
246 | @Override |
247 | public void truncate(Session session) { |
248 | TransactionMap<Value, Value> map = getMap(session); |
249 | if (mvTable.getContainsLargeObject()) { |
250 | database.getLobStorage().removeAllForTable(table.getId()); |
251 | } |
252 | map.clear(); |
253 | } |
254 | |
255 | @Override |
256 | public boolean canGetFirstOrLast() { |
257 | return true; |
258 | } |
259 | |
260 | @Override |
261 | public Cursor findFirstOrLast(Session session, boolean first) { |
262 | TransactionMap<Value, Value> map = getMap(session); |
263 | ValueLong v = (ValueLong) (first ? map.firstKey() : map.lastKey()); |
264 | if (v == null) { |
265 | return new MVStoreCursor(Collections |
266 | .<Entry<Value, Value>> emptyList().iterator(), null); |
267 | } |
268 | Value value = map.get(v); |
269 | Entry<Value, Value> e = new DataUtils.MapEntry<Value, Value>(v, value); |
270 | @SuppressWarnings("unchecked") |
271 | List<Entry<Value, Value>> list = Arrays.asList(e); |
272 | MVStoreCursor c = new MVStoreCursor(list.iterator(), v); |
273 | c.next(); |
274 | return c; |
275 | } |
276 | |
277 | @Override |
278 | public boolean needRebuild() { |
279 | return false; |
280 | } |
281 | |
282 | @Override |
283 | public long getRowCount(Session session) { |
284 | TransactionMap<Value, Value> map = getMap(session); |
285 | return map.sizeAsLong(); |
286 | } |
287 | |
288 | /** |
289 | * The maximum number of rows, including uncommitted rows of any session. |
290 | * |
291 | * @return the maximum number of rows |
292 | */ |
293 | public long getRowCountMax() { |
294 | try { |
295 | return dataMap.sizeAsLongMax(); |
296 | } catch (IllegalStateException e) { |
297 | throw DbException.get(ErrorCode.OBJECT_CLOSED, e); |
298 | } |
299 | } |
300 | |
301 | @Override |
302 | public long getRowCountApproximation() { |
303 | return getRowCountMax(); |
304 | } |
305 | |
306 | @Override |
307 | public long getDiskSpaceUsed() { |
308 | // TODO estimate disk space usage |
309 | return 0; |
310 | } |
311 | |
312 | public String getMapName() { |
313 | return mapName; |
314 | } |
315 | |
316 | @Override |
317 | public void checkRename() { |
318 | // ok |
319 | } |
320 | |
321 | /** |
322 | * Get the key from the row. |
323 | * |
324 | * @param row the row |
325 | * @param ifEmpty the value to use if the row is empty |
326 | * @param ifNull the value to use if the column is NULL |
327 | * @return the key |
328 | */ |
329 | ValueLong getKey(SearchRow row, ValueLong ifEmpty, ValueLong ifNull) { |
330 | if (row == null) { |
331 | return ifEmpty; |
332 | } |
333 | Value v = row.getValue(mainIndexColumn); |
334 | if (v == null) { |
335 | throw DbException.throwInternalError(row.toString()); |
336 | } else if (v == ValueNull.INSTANCE) { |
337 | return ifNull; |
338 | } |
339 | return (ValueLong) v.convertTo(Value.LONG); |
340 | } |
341 | |
342 | /** |
343 | * Search for a specific row or a set of rows. |
344 | * |
345 | * @param session the session |
346 | * @param first the key of the first row |
347 | * @param last the key of the last row |
348 | * @return the cursor |
349 | */ |
350 | Cursor find(Session session, ValueLong first, ValueLong last) { |
351 | TransactionMap<Value, Value> map = getMap(session); |
352 | return new MVStoreCursor(map.entryIterator(first), last); |
353 | } |
354 | |
355 | @Override |
356 | public boolean isRowIdIndex() { |
357 | return true; |
358 | } |
359 | |
360 | /** |
361 | * Get the map to store the data. |
362 | * |
363 | * @param session the session |
364 | * @return the map |
365 | */ |
366 | TransactionMap<Value, Value> getMap(Session session) { |
367 | if (session == null) { |
368 | return dataMap; |
369 | } |
370 | Transaction t = mvTable.getTransaction(session); |
371 | return dataMap.getInstance(t, Long.MAX_VALUE); |
372 | } |
373 | |
374 | /** |
375 | * A cursor. |
376 | */ |
377 | class MVStoreCursor implements Cursor { |
378 | |
379 | private final Iterator<Entry<Value, Value>> it; |
380 | private final ValueLong last; |
381 | private Entry<Value, Value> current; |
382 | private Row row; |
383 | |
384 | public MVStoreCursor(Iterator<Entry<Value, Value>> it, ValueLong last) { |
385 | this.it = it; |
386 | this.last = last; |
387 | } |
388 | |
389 | @Override |
390 | public Row get() { |
391 | if (row == null) { |
392 | if (current != null) { |
393 | ValueArray array = (ValueArray) current.getValue(); |
394 | row = new Row(array.getList(), 0); |
395 | row.setKey(current.getKey().getLong()); |
396 | } |
397 | } |
398 | return row; |
399 | } |
400 | |
401 | @Override |
402 | public SearchRow getSearchRow() { |
403 | return get(); |
404 | } |
405 | |
406 | @Override |
407 | public boolean next() { |
408 | current = it.hasNext() ? it.next() : null; |
409 | if (current != null && current.getKey().getLong() > last.getLong()) { |
410 | current = null; |
411 | } |
412 | row = null; |
413 | return current != null; |
414 | } |
415 | |
416 | @Override |
417 | public boolean previous() { |
418 | throw DbException.getUnsupportedException("previous"); |
419 | } |
420 | |
421 | } |
422 | |
423 | } |