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 | |
10 | import org.h2.api.ErrorCode; |
11 | import org.h2.command.dml.Query; |
12 | import org.h2.command.dml.SelectUnion; |
13 | import org.h2.engine.Constants; |
14 | import org.h2.engine.Session; |
15 | import org.h2.expression.Comparison; |
16 | import org.h2.expression.Parameter; |
17 | import org.h2.message.DbException; |
18 | import org.h2.result.LocalResult; |
19 | import org.h2.result.Row; |
20 | import org.h2.result.SearchRow; |
21 | import org.h2.result.SortOrder; |
22 | import org.h2.table.Column; |
23 | import org.h2.table.IndexColumn; |
24 | import org.h2.table.TableFilter; |
25 | import org.h2.table.TableView; |
26 | import org.h2.util.IntArray; |
27 | import org.h2.util.New; |
28 | import org.h2.util.SmallLRUCache; |
29 | import org.h2.util.SynchronizedVerifier; |
30 | import org.h2.util.Utils; |
31 | import org.h2.value.Value; |
32 | |
33 | /** |
34 | * This object represents a virtual index for a query. |
35 | * Actually it only represents a prepared SELECT statement. |
36 | */ |
37 | public class ViewIndex extends BaseIndex implements SpatialIndex { |
38 | |
39 | private final TableView view; |
40 | private final String querySQL; |
41 | private final ArrayList<Parameter> originalParameters; |
42 | private final SmallLRUCache<IntArray, CostElement> costCache = |
43 | SmallLRUCache.newInstance(Constants.VIEW_INDEX_CACHE_SIZE); |
44 | private boolean recursive; |
45 | private final int[] indexMasks; |
46 | private Query query; |
47 | private final Session createSession; |
48 | |
49 | public ViewIndex(TableView view, String querySQL, |
50 | ArrayList<Parameter> originalParameters, boolean recursive) { |
51 | initBaseIndex(view, 0, null, null, IndexType.createNonUnique(false)); |
52 | this.view = view; |
53 | this.querySQL = querySQL; |
54 | this.originalParameters = originalParameters; |
55 | this.recursive = recursive; |
56 | columns = new Column[0]; |
57 | this.createSession = null; |
58 | this.indexMasks = null; |
59 | } |
60 | |
61 | public ViewIndex(TableView view, ViewIndex index, Session session, |
62 | int[] masks) { |
63 | initBaseIndex(view, 0, null, null, IndexType.createNonUnique(false)); |
64 | this.view = view; |
65 | this.querySQL = index.querySQL; |
66 | this.originalParameters = index.originalParameters; |
67 | this.recursive = index.recursive; |
68 | this.indexMasks = masks; |
69 | this.createSession = session; |
70 | columns = new Column[0]; |
71 | if (!recursive) { |
72 | query = getQuery(session, masks); |
73 | } |
74 | } |
75 | |
76 | public Session getSession() { |
77 | return createSession; |
78 | } |
79 | |
80 | @Override |
81 | public String getPlanSQL() { |
82 | return query == null ? null : query.getPlanSQL(); |
83 | } |
84 | |
85 | @Override |
86 | public void close(Session session) { |
87 | // nothing to do |
88 | } |
89 | |
90 | @Override |
91 | public void add(Session session, Row row) { |
92 | throw DbException.getUnsupportedException("VIEW"); |
93 | } |
94 | |
95 | @Override |
96 | public void remove(Session session, Row row) { |
97 | throw DbException.getUnsupportedException("VIEW"); |
98 | } |
99 | |
100 | /** |
101 | * A calculated cost value. |
102 | */ |
103 | static class CostElement { |
104 | |
105 | /** |
106 | * The time in milliseconds when this cost was calculated. |
107 | */ |
108 | long evaluatedAt; |
109 | |
110 | /** |
111 | * The cost. |
112 | */ |
113 | double cost; |
114 | } |
115 | |
116 | @Override |
117 | public synchronized double getCost(Session session, int[] masks, |
118 | TableFilter filter, SortOrder sortOrder) { |
119 | if (recursive) { |
120 | return 1000; |
121 | } |
122 | IntArray masksArray = new IntArray(masks == null ? |
123 | Utils.EMPTY_INT_ARRAY : masks); |
124 | SynchronizedVerifier.check(costCache); |
125 | CostElement cachedCost = costCache.get(masksArray); |
126 | if (cachedCost != null) { |
127 | long time = System.currentTimeMillis(); |
128 | if (time < cachedCost.evaluatedAt + Constants.VIEW_COST_CACHE_MAX_AGE) { |
129 | return cachedCost.cost; |
130 | } |
131 | } |
132 | Query q = (Query) session.prepare(querySQL, true); |
133 | if (masks != null) { |
134 | IntArray paramIndex = new IntArray(); |
135 | for (int i = 0; i < masks.length; i++) { |
136 | int mask = masks[i]; |
137 | if (mask == 0) { |
138 | continue; |
139 | } |
140 | paramIndex.add(i); |
141 | } |
142 | int len = paramIndex.size(); |
143 | for (int i = 0; i < len; i++) { |
144 | int idx = paramIndex.get(i); |
145 | int mask = masks[idx]; |
146 | int nextParamIndex = q.getParameters().size() + view.getParameterOffset(); |
147 | if ((mask & IndexCondition.EQUALITY) != 0) { |
148 | Parameter param = new Parameter(nextParamIndex); |
149 | q.addGlobalCondition(param, idx, Comparison.EQUAL_NULL_SAFE); |
150 | } else if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { |
151 | Parameter param = new Parameter(nextParamIndex); |
152 | q.addGlobalCondition(param, idx, Comparison.SPATIAL_INTERSECTS); |
153 | } else { |
154 | if ((mask & IndexCondition.START) != 0) { |
155 | Parameter param = new Parameter(nextParamIndex); |
156 | q.addGlobalCondition(param, idx, Comparison.BIGGER_EQUAL); |
157 | } |
158 | if ((mask & IndexCondition.END) != 0) { |
159 | Parameter param = new Parameter(nextParamIndex); |
160 | q.addGlobalCondition(param, idx, Comparison.SMALLER_EQUAL); |
161 | } |
162 | } |
163 | } |
164 | String sql = q.getPlanSQL(); |
165 | q = (Query) session.prepare(sql, true); |
166 | } |
167 | double cost = q.getCost(); |
168 | cachedCost = new CostElement(); |
169 | cachedCost.evaluatedAt = System.currentTimeMillis(); |
170 | cachedCost.cost = cost; |
171 | costCache.put(masksArray, cachedCost); |
172 | return cost; |
173 | } |
174 | |
175 | @Override |
176 | public Cursor find(Session session, SearchRow first, SearchRow last) { |
177 | return find(session, first, last, null); |
178 | } |
179 | |
180 | @Override |
181 | public Cursor findByGeometry(TableFilter filter, SearchRow intersection) { |
182 | return find(filter.getSession(), null, null, intersection); |
183 | } |
184 | |
185 | private Cursor find(Session session, SearchRow first, SearchRow last, |
186 | SearchRow intersection) { |
187 | if (recursive) { |
188 | LocalResult recResult = view.getRecursiveResult(); |
189 | if (recResult != null) { |
190 | recResult.reset(); |
191 | return new ViewCursor(this, recResult, first, last); |
192 | } |
193 | if (query == null) { |
194 | query = (Query) createSession.prepare(querySQL, true); |
195 | } |
196 | if (!(query instanceof SelectUnion)) { |
197 | throw DbException.get(ErrorCode.SYNTAX_ERROR_2, |
198 | "recursive queries without UNION ALL"); |
199 | } |
200 | SelectUnion union = (SelectUnion) query; |
201 | if (union.getUnionType() != SelectUnion.UNION_ALL) { |
202 | throw DbException.get(ErrorCode.SYNTAX_ERROR_2, |
203 | "recursive queries without UNION ALL"); |
204 | } |
205 | Query left = union.getLeft(); |
206 | // to ensure the last result is not closed |
207 | left.disableCache(); |
208 | LocalResult r = left.query(0); |
209 | LocalResult result = union.getEmptyResult(); |
210 | // ensure it is not written to disk, |
211 | // because it is not closed normally |
212 | result.setMaxMemoryRows(Integer.MAX_VALUE); |
213 | while (r.next()) { |
214 | result.addRow(r.currentRow()); |
215 | } |
216 | Query right = union.getRight(); |
217 | r.reset(); |
218 | view.setRecursiveResult(r); |
219 | // to ensure the last result is not closed |
220 | right.disableCache(); |
221 | while (true) { |
222 | r = right.query(0); |
223 | if (r.getRowCount() == 0) { |
224 | break; |
225 | } |
226 | while (r.next()) { |
227 | result.addRow(r.currentRow()); |
228 | } |
229 | r.reset(); |
230 | view.setRecursiveResult(r); |
231 | } |
232 | view.setRecursiveResult(null); |
233 | result.done(); |
234 | return new ViewCursor(this, result, first, last); |
235 | } |
236 | ArrayList<Parameter> paramList = query.getParameters(); |
237 | if (originalParameters != null) { |
238 | for (int i = 0, size = originalParameters.size(); i < size; i++) { |
239 | Parameter orig = originalParameters.get(i); |
240 | int idx = orig.getIndex(); |
241 | Value value = orig.getValue(session); |
242 | setParameter(paramList, idx, value); |
243 | } |
244 | } |
245 | int len; |
246 | if (first != null) { |
247 | len = first.getColumnCount(); |
248 | } else if (last != null) { |
249 | len = last.getColumnCount(); |
250 | } else if (intersection != null) { |
251 | len = intersection.getColumnCount(); |
252 | } else { |
253 | len = 0; |
254 | } |
255 | int idx = originalParameters == null ? 0 : originalParameters.size(); |
256 | idx += view.getParameterOffset(); |
257 | for (int i = 0; i < len; i++) { |
258 | int mask = indexMasks[i]; |
259 | if ((mask & IndexCondition.EQUALITY) != 0) { |
260 | setParameter(paramList, idx++, first.getValue(i)); |
261 | } |
262 | if ((mask & IndexCondition.START) != 0) { |
263 | setParameter(paramList, idx++, first.getValue(i)); |
264 | } |
265 | if ((mask & IndexCondition.END) != 0) { |
266 | setParameter(paramList, idx++, last.getValue(i)); |
267 | } |
268 | if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { |
269 | setParameter(paramList, idx++, intersection.getValue(i)); |
270 | } |
271 | } |
272 | LocalResult result = query.query(0); |
273 | return new ViewCursor(this, result, first, last); |
274 | } |
275 | |
276 | private static void setParameter(ArrayList<Parameter> paramList, int x, |
277 | Value v) { |
278 | if (x >= paramList.size()) { |
279 | // the parameter may be optimized away as in |
280 | // select * from (select null as x) where x=1; |
281 | return; |
282 | } |
283 | Parameter param = paramList.get(x); |
284 | param.setValue(v); |
285 | } |
286 | |
287 | private Query getQuery(Session session, int[] masks) { |
288 | Query q = (Query) session.prepare(querySQL, true); |
289 | if (masks == null) { |
290 | return q; |
291 | } |
292 | if (!q.allowGlobalConditions()) { |
293 | return q; |
294 | } |
295 | int firstIndexParam = originalParameters == null ? |
296 | 0 : originalParameters.size(); |
297 | firstIndexParam += view.getParameterOffset(); |
298 | IntArray paramIndex = new IntArray(); |
299 | int indexColumnCount = 0; |
300 | for (int i = 0; i < masks.length; i++) { |
301 | int mask = masks[i]; |
302 | if (mask == 0) { |
303 | continue; |
304 | } |
305 | indexColumnCount++; |
306 | paramIndex.add(i); |
307 | if (Integer.bitCount(mask) > 1) { |
308 | // two parameters for range queries: >= x AND <= y |
309 | paramIndex.add(i); |
310 | } |
311 | } |
312 | int len = paramIndex.size(); |
313 | ArrayList<Column> columnList = New.arrayList(); |
314 | for (int i = 0; i < len;) { |
315 | int idx = paramIndex.get(i); |
316 | columnList.add(table.getColumn(idx)); |
317 | int mask = masks[idx]; |
318 | if ((mask & IndexCondition.EQUALITY) != 0) { |
319 | Parameter param = new Parameter(firstIndexParam + i); |
320 | q.addGlobalCondition(param, idx, Comparison.EQUAL_NULL_SAFE); |
321 | i++; |
322 | } |
323 | if ((mask & IndexCondition.START) != 0) { |
324 | Parameter param = new Parameter(firstIndexParam + i); |
325 | q.addGlobalCondition(param, idx, Comparison.BIGGER_EQUAL); |
326 | i++; |
327 | } |
328 | if ((mask & IndexCondition.END) != 0) { |
329 | Parameter param = new Parameter(firstIndexParam + i); |
330 | q.addGlobalCondition(param, idx, Comparison.SMALLER_EQUAL); |
331 | i++; |
332 | } |
333 | if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { |
334 | Parameter param = new Parameter(firstIndexParam + i); |
335 | q.addGlobalCondition(param, idx, Comparison.SPATIAL_INTERSECTS); |
336 | i++; |
337 | } |
338 | } |
339 | columns = new Column[columnList.size()]; |
340 | columnList.toArray(columns); |
341 | |
342 | // reconstruct the index columns from the masks |
343 | this.indexColumns = new IndexColumn[indexColumnCount]; |
344 | this.columnIds = new int[indexColumnCount]; |
345 | for (int type = 0, indexColumnId = 0; type < 2; type++) { |
346 | for (int i = 0; i < masks.length; i++) { |
347 | int mask = masks[i]; |
348 | if (mask == 0) { |
349 | continue; |
350 | } |
351 | if (type == 0) { |
352 | if ((mask & IndexCondition.EQUALITY) == 0) { |
353 | // the first columns need to be equality conditions |
354 | continue; |
355 | } |
356 | } else { |
357 | if ((mask & IndexCondition.EQUALITY) != 0) { |
358 | // after that only range conditions |
359 | continue; |
360 | } |
361 | } |
362 | IndexColumn c = new IndexColumn(); |
363 | c.column = table.getColumn(i); |
364 | indexColumns[indexColumnId] = c; |
365 | columnIds[indexColumnId] = c.column.getColumnId(); |
366 | indexColumnId++; |
367 | } |
368 | } |
369 | |
370 | String sql = q.getPlanSQL(); |
371 | q = (Query) session.prepare(sql, true); |
372 | return q; |
373 | } |
374 | |
375 | @Override |
376 | public void remove(Session session) { |
377 | throw DbException.getUnsupportedException("VIEW"); |
378 | } |
379 | |
380 | @Override |
381 | public void truncate(Session session) { |
382 | throw DbException.getUnsupportedException("VIEW"); |
383 | } |
384 | |
385 | @Override |
386 | public void checkRename() { |
387 | throw DbException.getUnsupportedException("VIEW"); |
388 | } |
389 | |
390 | @Override |
391 | public boolean needRebuild() { |
392 | return false; |
393 | } |
394 | |
395 | @Override |
396 | public boolean canGetFirstOrLast() { |
397 | return false; |
398 | } |
399 | |
400 | @Override |
401 | public Cursor findFirstOrLast(Session session, boolean first) { |
402 | throw DbException.getUnsupportedException("VIEW"); |
403 | } |
404 | |
405 | public void setRecursive(boolean value) { |
406 | this.recursive = value; |
407 | } |
408 | |
409 | @Override |
410 | public long getRowCount(Session session) { |
411 | return 0; |
412 | } |
413 | |
414 | @Override |
415 | public long getRowCountApproximation() { |
416 | return 0; |
417 | } |
418 | |
419 | @Override |
420 | public long getDiskSpaceUsed() { |
421 | return 0; |
422 | } |
423 | |
424 | public boolean isRecursive() { |
425 | return recursive; |
426 | } |
427 | |
428 | } |