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.index; |
7 | |
8 | import java.util.ArrayList; |
9 | import java.util.Collections; |
10 | import java.util.HashMap; |
11 | import java.util.HashSet; |
12 | import java.util.Iterator; |
13 | import java.util.List; |
14 | |
15 | import org.h2.api.ErrorCode; |
16 | import org.h2.engine.Constants; |
17 | import org.h2.engine.Session; |
18 | import org.h2.engine.UndoLogRecord; |
19 | import org.h2.message.DbException; |
20 | import org.h2.result.Row; |
21 | import org.h2.result.SearchRow; |
22 | import org.h2.result.SortOrder; |
23 | import org.h2.table.Column; |
24 | import org.h2.table.IndexColumn; |
25 | import org.h2.table.RegularTable; |
26 | import org.h2.table.TableFilter; |
27 | import org.h2.util.New; |
28 | |
29 | /** |
30 | * The scan index is not really an 'index' in the strict sense, because it can |
31 | * not be used for direct lookup. It can only be used to iterate over all rows |
32 | * of a table. Each regular table has one such object, even if no primary key or |
33 | * indexes are defined. |
34 | */ |
35 | public class ScanIndex extends BaseIndex { |
36 | private long firstFree = -1; |
37 | private ArrayList<Row> rows = New.arrayList(); |
38 | private final RegularTable tableData; |
39 | private int rowCountDiff; |
40 | private final HashMap<Integer, Integer> sessionRowCount; |
41 | private HashSet<Row> delta; |
42 | private long rowCount; |
43 | |
44 | public ScanIndex(RegularTable table, int id, IndexColumn[] columns, |
45 | IndexType indexType) { |
46 | initBaseIndex(table, id, table.getName() + "_DATA", columns, indexType); |
47 | if (database.isMultiVersion()) { |
48 | sessionRowCount = New.hashMap(); |
49 | } else { |
50 | sessionRowCount = null; |
51 | } |
52 | tableData = table; |
53 | } |
54 | |
55 | @Override |
56 | public void remove(Session session) { |
57 | truncate(session); |
58 | } |
59 | |
60 | @Override |
61 | public void truncate(Session session) { |
62 | rows = New.arrayList(); |
63 | firstFree = -1; |
64 | if (tableData.getContainsLargeObject() && tableData.isPersistData()) { |
65 | database.getLobStorage().removeAllForTable(table.getId()); |
66 | } |
67 | tableData.setRowCount(0); |
68 | rowCount = 0; |
69 | rowCountDiff = 0; |
70 | if (database.isMultiVersion()) { |
71 | sessionRowCount.clear(); |
72 | } |
73 | } |
74 | |
75 | @Override |
76 | public String getCreateSQL() { |
77 | return null; |
78 | } |
79 | |
80 | @Override |
81 | public void close(Session session) { |
82 | // nothing to do |
83 | } |
84 | |
85 | @Override |
86 | public Row getRow(Session session, long key) { |
87 | return rows.get((int) key); |
88 | } |
89 | |
90 | @Override |
91 | public void add(Session session, Row row) { |
92 | // in-memory |
93 | if (firstFree == -1) { |
94 | int key = rows.size(); |
95 | row.setKey(key); |
96 | rows.add(row); |
97 | } else { |
98 | long key = firstFree; |
99 | Row free = rows.get((int) key); |
100 | firstFree = free.getKey(); |
101 | row.setKey(key); |
102 | rows.set((int) key, row); |
103 | } |
104 | row.setDeleted(false); |
105 | if (database.isMultiVersion()) { |
106 | if (delta == null) { |
107 | delta = New.hashSet(); |
108 | } |
109 | boolean wasDeleted = delta.remove(row); |
110 | if (!wasDeleted) { |
111 | delta.add(row); |
112 | } |
113 | incrementRowCount(session.getId(), 1); |
114 | } |
115 | rowCount++; |
116 | } |
117 | |
118 | @Override |
119 | public void commit(int operation, Row row) { |
120 | if (database.isMultiVersion()) { |
121 | if (delta != null) { |
122 | delta.remove(row); |
123 | } |
124 | incrementRowCount(row.getSessionId(), |
125 | operation == UndoLogRecord.DELETE ? 1 : -1); |
126 | } |
127 | } |
128 | |
129 | private void incrementRowCount(int sessionId, int count) { |
130 | if (database.isMultiVersion()) { |
131 | Integer id = sessionId; |
132 | Integer c = sessionRowCount.get(id); |
133 | int current = c == null ? 0 : c.intValue(); |
134 | sessionRowCount.put(id, current + count); |
135 | rowCountDiff += count; |
136 | } |
137 | } |
138 | |
139 | @Override |
140 | public void remove(Session session, Row row) { |
141 | // in-memory |
142 | if (!database.isMultiVersion() && rowCount == 1) { |
143 | rows = New.arrayList(); |
144 | firstFree = -1; |
145 | } else { |
146 | Row free = new Row(null, 1); |
147 | free.setKey(firstFree); |
148 | long key = row.getKey(); |
149 | if (rows.size() <= key) { |
150 | throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1, |
151 | rows.size() + ": " + key); |
152 | } |
153 | rows.set((int) key, free); |
154 | firstFree = key; |
155 | } |
156 | if (database.isMultiVersion()) { |
157 | // if storage is null, the delete flag is not yet set |
158 | row.setDeleted(true); |
159 | if (delta == null) { |
160 | delta = New.hashSet(); |
161 | } |
162 | boolean wasAdded = delta.remove(row); |
163 | if (!wasAdded) { |
164 | delta.add(row); |
165 | } |
166 | incrementRowCount(session.getId(), -1); |
167 | } |
168 | rowCount--; |
169 | } |
170 | |
171 | @Override |
172 | public Cursor find(Session session, SearchRow first, SearchRow last) { |
173 | return new ScanCursor(session, this, database.isMultiVersion()); |
174 | } |
175 | |
176 | @Override |
177 | public double getCost(Session session, int[] masks, TableFilter filter, |
178 | SortOrder sortOrder) { |
179 | return tableData.getRowCountApproximation() + Constants.COST_ROW_OFFSET; |
180 | } |
181 | |
182 | @Override |
183 | public long getRowCount(Session session) { |
184 | if (database.isMultiVersion()) { |
185 | Integer i = sessionRowCount.get(session.getId()); |
186 | long count = i == null ? 0 : i.intValue(); |
187 | count += rowCount; |
188 | count -= rowCountDiff; |
189 | return count; |
190 | } |
191 | return rowCount; |
192 | } |
193 | |
194 | /** |
195 | * Get the next row that is stored after this row. |
196 | * |
197 | * @param row the current row or null to start the scan |
198 | * @return the next row or null if there are no more rows |
199 | */ |
200 | Row getNextRow(Row row) { |
201 | long key; |
202 | if (row == null) { |
203 | key = -1; |
204 | } else { |
205 | key = row.getKey(); |
206 | } |
207 | while (true) { |
208 | key++; |
209 | if (key >= rows.size()) { |
210 | return null; |
211 | } |
212 | row = rows.get((int) key); |
213 | if (!row.isEmpty()) { |
214 | return row; |
215 | } |
216 | } |
217 | } |
218 | |
219 | @Override |
220 | public int getColumnIndex(Column col) { |
221 | // the scan index cannot use any columns |
222 | return -1; |
223 | } |
224 | |
225 | @Override |
226 | public void checkRename() { |
227 | throw DbException.getUnsupportedException("SCAN"); |
228 | } |
229 | |
230 | @Override |
231 | public boolean needRebuild() { |
232 | return false; |
233 | } |
234 | |
235 | @Override |
236 | public boolean canGetFirstOrLast() { |
237 | return false; |
238 | } |
239 | |
240 | @Override |
241 | public Cursor findFirstOrLast(Session session, boolean first) { |
242 | throw DbException.getUnsupportedException("SCAN"); |
243 | } |
244 | |
245 | Iterator<Row> getDelta() { |
246 | if (delta == null) { |
247 | List<Row> e = Collections.emptyList(); |
248 | return e.iterator(); |
249 | } |
250 | return delta.iterator(); |
251 | } |
252 | |
253 | @Override |
254 | public long getRowCountApproximation() { |
255 | return rowCount; |
256 | } |
257 | |
258 | @Override |
259 | public long getDiskSpaceUsed() { |
260 | return 0; |
261 | } |
262 | |
263 | @Override |
264 | public String getPlanSQL() { |
265 | return table.getSQL() + ".tableScan"; |
266 | } |
267 | |
268 | } |