/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ignite.internal.processors.query.h2.opt; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.internal.processors.query.h2.H2Cursor; import org.apache.ignite.internal.util.GridCursorIteratorWrapper; import org.apache.ignite.internal.util.IgniteTree; import org.apache.ignite.internal.util.lang.GridCursor; import org.h2.engine.Session; import org.h2.index.Cursor; import org.h2.index.IndexLookupBatch; import org.h2.index.IndexType; import org.h2.index.SingleRowCursor; import org.h2.index.SpatialIndex; import org.h2.index.SpatialTreeIndex; import org.h2.message.DbException; import org.h2.mvstore.MVStore; import org.h2.mvstore.rtree.MVRTreeMap; import org.h2.mvstore.rtree.SpatialKey; import org.h2.result.SearchRow; import org.h2.result.SortOrder; import org.h2.table.Column; import org.h2.table.IndexColumn; import org.h2.table.TableFilter; import org.h2.value.Value; import org.h2.value.ValueGeometry; import org.jetbrains.annotations.Nullable; import static org.apache.ignite.internal.processors.query.h2.opt.GridH2AbstractKeyValueRow.KEY_COL; /** * Spatial index. */ @SuppressWarnings("unused"/*reflection*/) public class GridH2SpatialIndex extends GridH2IndexBase implements SpatialIndex { /** */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); /** */ private volatile long rowCnt; /** */ private long rowIds; /** */ private boolean closed; /** */ private final MVRTreeMap<Long>[] segments; /** */ private final Map<Long, GridH2Row> idToRow = new HashMap<>(); /** */ private final Map<Value, Long> keyToId = new HashMap<>(); /** */ private final MVStore store; /** * @param tbl Table. * @param idxName Index name. * @param cols Columns. */ public GridH2SpatialIndex(GridH2Table tbl, String idxName, IndexColumn... cols) { this(tbl, idxName, 1, cols); } /** * @param tbl Table. * @param idxName Index name. * @param segmentsCnt Index segments count. * @param cols Columns. */ public GridH2SpatialIndex(GridH2Table tbl, String idxName, int segmentsCnt, IndexColumn... cols) { if (cols.length > 1) throw DbException.getUnsupportedException("can only do one column"); if ((cols[0].sortType & SortOrder.DESCENDING) != 0) throw DbException.getUnsupportedException("cannot do descending"); if ((cols[0].sortType & SortOrder.NULLS_FIRST) != 0) throw DbException.getUnsupportedException("cannot do nulls first"); if ((cols[0].sortType & SortOrder.NULLS_LAST) != 0) throw DbException.getUnsupportedException("cannot do nulls last"); initBaseIndex(tbl, 0, idxName, cols, IndexType.createNonUnique(false, false, true)); table = tbl; if (cols[0].column.getType() != Value.GEOMETRY) { throw DbException.getUnsupportedException("spatial index on non-geometry column, " + cols[0].column.getCreateSQL()); } // Index in memory store = MVStore.open(null); segments = new MVRTreeMap[segmentsCnt]; for (int i = 0; i < segmentsCnt; i++) segments[i] = store.openMap("spatialIndex-" + i, new MVRTreeMap.Builder<Long>()); ctx = tbl.rowDescriptor().context(); } /** {@inheritDoc} */ @Override public IndexLookupBatch createLookupBatch(TableFilter[] filters, int filter) { if (getTable().isPartitioned()) { assert filter > 0; // Lookup batch will not be created for the first table filter. throw DbException.throwInternalError( "Table with a spatial index must be the first in the query: " + getTable()); } return null; // Support must be explicitly added. } /** * Check closed. */ private void checkClosed() { if (closed) throw DbException.throwInternalError(); } /** {@inheritDoc} */ @Override protected int segmentsCount() { return segments.length; } /** {@inheritDoc} */ @Nullable @Override protected IgniteTree doTakeSnapshot() { return null; // TODO We do not support snapshots, but probably this is possible. } /** {@inheritDoc} */ @Override public GridH2Row put(GridH2Row row) { assert row instanceof GridH2AbstractKeyValueRow : "requires key to be at 0"; Lock l = lock.writeLock(); l.lock(); try { checkClosed(); Value key = row.getValue(KEY_COL); assert key != null; final int seg = segmentForRow(row); Long rowId = keyToId.get(key); if (rowId != null) { Long oldRowId = segments[seg].remove(getEnvelope(idToRow.get(rowId), rowId)); assert rowId.equals(oldRowId); } else { rowId = ++rowIds; keyToId.put(key, rowId); } GridH2Row old = idToRow.put(rowId, row); segments[seg].put(getEnvelope(row, rowId), rowId); if (old == null) rowCnt++; // No replace. return old; } finally { l.unlock(); } } /** * @param row Row. * @param rowId Row id. * @return Envelope. */ private SpatialKey getEnvelope(SearchRow row, long rowId) { Value v = row.getValue(columnIds[0]); Geometry g = ((ValueGeometry) v.convertTo(Value.GEOMETRY)).getGeometry(); Envelope env = g.getEnvelopeInternal(); return new SpatialKey(rowId, (float) env.getMinX(), (float) env.getMaxX(), (float) env.getMinY(), (float) env.getMaxY()); } /** {@inheritDoc} */ @Override public GridH2Row remove(SearchRow row) { Lock l = lock.writeLock(); l.lock(); try { checkClosed(); Value key = row.getValue(KEY_COL); assert key != null; Long rowId = keyToId.remove(key); assert rowId != null; GridH2Row oldRow = idToRow.remove(rowId); assert oldRow != null; final int seg = segmentForRow(row); if (!segments[seg].remove(getEnvelope(row, rowId), rowId)) throw DbException.throwInternalError("row not found"); rowCnt--; return oldRow; } finally { l.unlock(); } } /** {@inheritDoc} */ @Override public void destroy() { Lock l = lock.writeLock(); l.lock(); try { closed = true; store.close(); } finally { l.unlock(); } super.destroy(); } /** {@inheritDoc} */ @Override public double getCost(Session ses, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, HashSet<Column> cols) { return SpatialTreeIndex.getCostRangeIndex(masks, table.getRowCountApproximation(), columns) / 10; } /** {@inheritDoc} */ @Override public Cursor find(TableFilter filter, SearchRow first, SearchRow last) { return find0(filter); } /** {@inheritDoc} */ @Override public Cursor find(Session ses, SearchRow first, SearchRow last) { return find0(null); } /** * @param filter Table filter. * @return Cursor. */ private Cursor find0(TableFilter filter) { Lock l = lock.readLock(); l.lock(); try { checkClosed(); final int seg = threadLocalSegment(); final MVRTreeMap<Long> segment = segments[seg]; return new H2Cursor(rowIterator(segment.keySet().iterator(), filter)); } finally { l.unlock(); } } /** {@inheritDoc} */ @Override public GridH2Row findOne(GridH2Row row) { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public boolean canGetFirstOrLast() { return true; } /** * @param i Spatial key iterator. * @param filter Table filter. * @return Iterator over rows. */ private GridCursor<GridH2Row> rowIterator(Iterator<SpatialKey> i, TableFilter filter) { if (!i.hasNext()) return EMPTY_CURSOR; List<GridH2Row> rows = new ArrayList<>(); do { GridH2Row row = idToRow.get(i.next().getId()); assert row != null; rows.add(row); } while (i.hasNext()); return filter(new GridCursorIteratorWrapper(rows.iterator()), threadLocalFilter()); } /** {@inheritDoc} */ @Override public Cursor findFirstOrLast(Session ses, boolean first) { Lock l = lock.readLock(); l.lock(); try { checkClosed(); if (!first) throw DbException.throwInternalError("Spatial Index can only be fetch by ascending order"); final int seg = threadLocalSegment(); final MVRTreeMap<Long> segment = segments[seg]; GridCursor<GridH2Row> iter = rowIterator(segment.keySet().iterator(), null); return new SingleRowCursor(iter.next() ? iter.get() : null); } catch (IgniteCheckedException e) { throw DbException.convert(e); } finally { l.unlock(); } } /** {@inheritDoc} */ @Override public long getRowCount(Session ses) { return rowCnt; } /** {@inheritDoc} */ @Override public long getRowCountApproximation() { return rowCnt; } /** {@inheritDoc} */ @Override public Cursor findByGeometry(TableFilter filter, SearchRow first, SearchRow last, SearchRow intersection) { Lock l = lock.readLock(); l.lock(); try { if (intersection == null) return find(filter.getSession(), null, null); final int seg = threadLocalSegment(); final MVRTreeMap<Long> segment = segments[seg]; return new H2Cursor(rowIterator(segment.findIntersectingKeys(getEnvelope(intersection, 0)), filter)); } finally { l.unlock(); } } }