提交 99655cf1 authored 作者: noelgrandin's avatar noelgrandin

Add support for in-memory spatial index.

上级 18cfcf6c
......@@ -42,9 +42,10 @@ Change Log
</li><li>Issue 479: Support for SUBSTRING without a FROM condition, patch from Andrew Franklin.
</li><li>Issue 472: PgServer does not work with any recent Postgres JDBC driver, patch from Andrew Franklin.
</li><li>Add syntax for passing additional parameters into custom TableEngine implementations.
</li><li>Add support for spatial datatype GEOMETRY.
</li><li>Issue 480: Bugfix post issue #475, #477, patch from Andrew Franklin.
</li><li>Issue 481: Further extensions to PgServer to support better support PG JDBC, patch from Andrew Franklin.
</li><li>Add support for spatial datatype GEOMETRY.
</li><li>Add support for in-memory spatial index.
</li></ul>
<h2>Version 1.3.172 (2013-05-25)</h2>
......
......@@ -1889,6 +1889,14 @@ public class Parser {
read(")");
return new ConditionExists(query);
}
if (readIf("INTERSECTS")) {
read("(");
Expression r1 = readConcat();
read(",");
Expression r2 = readConcat();
read(")");
return new Comparison(session, Comparison.SPATIAL_INTERSECTS, r1, r2);
}
Expression r = readConcat();
while (true) {
// special case: NOT NULL is not part of an expression (as in CREATE
......@@ -3863,7 +3871,7 @@ public class Parser {
}
return parseCreateTable(false, false, cached);
} else {
boolean hash = false, primaryKey = false, unique = false;
boolean hash = false, primaryKey = false, unique = false, spatial = false;;
String indexName = null;
Schema oldSchema = null;
boolean ifNotExists = false;
......@@ -3885,6 +3893,9 @@ public class Parser {
if (readIf("HASH")) {
hash = true;
}
if (readIf("SPATIAL")) {
spatial = true;
}
if (readIf("INDEX")) {
if (!isToken("ON")) {
ifNotExists = readIfNoExists();
......@@ -3901,6 +3912,7 @@ public class Parser {
CreateIndex command = new CreateIndex(session, getSchema());
command.setIfNotExists(ifNotExists);
command.setHash(hash);
command.setSpatial(spatial);
command.setPrimaryKey(primaryKey);
command.setTableName(tableName);
command.setUnique(unique);
......
......@@ -27,7 +27,7 @@ public class CreateIndex extends SchemaCommand {
private String tableName;
private String indexName;
private IndexColumn[] indexColumns;
private boolean primaryKey, unique, hash;
private boolean primaryKey, unique, hash, spatial;
private boolean ifNotExists;
private String comment;
......@@ -87,7 +87,7 @@ public class CreateIndex extends SchemaCommand {
} else if (unique) {
indexType = IndexType.createUnique(persistent, hash);
} else {
indexType = IndexType.createNonUnique(persistent, hash);
indexType = IndexType.createNonUnique(persistent, hash, spatial);
}
IndexColumn.mapColumns(indexColumns, table);
table.addIndex(session, indexName, id, indexColumns, indexType, create, comment);
......@@ -106,6 +106,10 @@ public class CreateIndex extends SchemaCommand {
this.hash = b;
}
public void setSpatial(boolean b) {
this.spatial = b;
}
public void setComment(String comment) {
this.comment = comment;
}
......
......@@ -17,6 +17,7 @@ import org.h2.table.TableFilter;
import org.h2.util.New;
import org.h2.value.Value;
import org.h2.value.ValueBoolean;
import org.h2.value.ValueGeometry;
import org.h2.value.ValueNull;
/**
......@@ -99,6 +100,11 @@ public class Comparison extends Condition {
*/
public static final int IN_QUERY = 10;
/**
* This is a pseudo comparison type that is only used for spatial index conditions.
*/
public static final int SPATIAL_INTERSECTS = 11;
private final Database database;
private int compareType;
private Expression left;
......@@ -121,6 +127,9 @@ public class Comparison extends Condition {
case IS_NOT_NULL:
sql = left.getSQL() + " IS NOT NULL";
break;
case SPATIAL_INTERSECTS:
sql = "INTERSECTS(" + left.getSQL() + ", " + right.getSQL() + ")";
break;
default:
sql = left.getSQL() + " " + getCompareOperator(compareType) + " " + right.getSQL();
}
......@@ -272,6 +281,12 @@ public class Comparison extends Condition {
case SMALLER:
result = database.compare(l, r) < 0;
break;
case SPATIAL_INTERSECTS: {
ValueGeometry lg = (ValueGeometry) l.convertTo(Value.GEOMETRY);
ValueGeometry rg = (ValueGeometry) r.convertTo(Value.GEOMETRY);
result = lg.intersects(rg);
break;
}
default:
throw DbException.throwInternalError("type=" + compareType);
}
......@@ -392,6 +407,7 @@ public class Comparison extends Condition {
case BIGGER_EQUAL:
case SMALLER_EQUAL:
case SMALLER:
case SPATIAL_INTERSECTS:
addIndex = true;
break;
default:
......
......@@ -57,6 +57,11 @@ public class IndexCondition {
*/
public static final int ALWAYS_FALSE = 8;
/**
* A bit of a search mask meaning 'spatial intersection'.
*/
public static final int SPATIAL_INTERSECTS = 16;
private final Column column;
/**
* see constants in {@link Comparison}
......@@ -249,6 +254,8 @@ public class IndexCondition {
case Comparison.SMALLER_EQUAL:
case Comparison.SMALLER:
return END;
case Comparison.SPATIAL_INTERSECTS:
return SPATIAL_INTERSECTS;
default:
throw DbException.throwInternalError("type=" + compareType);
}
......@@ -299,6 +306,20 @@ public class IndexCondition {
}
}
/**
* Check if this index condition is of the type spatial column intersects value.
*
* @return true if this is a spatial intersects condition
*/
public boolean isSpatialIntersects() {
switch (compareType) {
case Comparison.SPATIAL_INTERSECTS:
return true;
default:
return false;
}
}
public int getCompareType() {
return compareType;
}
......
......@@ -20,6 +20,7 @@ import org.h2.table.IndexColumn;
import org.h2.table.Table;
import org.h2.table.TableFilter;
import org.h2.value.Value;
import org.h2.value.ValueGeometry;
import org.h2.value.ValueNull;
/**
......@@ -35,7 +36,7 @@ public class IndexCursor implements Cursor {
private IndexColumn[] indexColumns;
private boolean alwaysFalse;
private SearchRow start, end;
private SearchRow start, end, intersects;
private Cursor cursor;
private Column inColumn;
private int inListIndex;
......@@ -104,6 +105,7 @@ public class IndexCursor implements Cursor {
Value v = condition.getCurrentValue(s);
boolean isStart = condition.isStart();
boolean isEnd = condition.isEnd();
boolean isIntersects = condition.isSpatialIntersects();
int columnId = column.getColumnId();
if (columnId >= 0) {
IndexColumn idxCol = indexColumns[columnId];
......@@ -121,6 +123,9 @@ public class IndexCursor implements Cursor {
if (isEnd) {
end = getSearchRow(end, columnId, v, false);
}
if (isIntersects) {
intersects = getSpatialSearchRow(intersects, columnId, v, true);
}
if (isStart || isEnd) {
// an X=? condition will produce less rows than
// an X IN(..) condition
......@@ -142,7 +147,11 @@ public class IndexCursor implements Cursor {
return;
}
if (!alwaysFalse) {
cursor = index.find(tableFilter, start, end);
if (intersects != null && index instanceof SpatialTreeIndex) {
cursor = ((SpatialTreeIndex)index).findByGeometry(tableFilter, intersects);
} else {
cursor = index.find(tableFilter, start, end);
}
}
}
......@@ -163,6 +172,25 @@ public class IndexCursor implements Cursor {
return idxCol == null || idxCol.column == column;
}
private SearchRow getSpatialSearchRow(SearchRow row, int columnId, Value v, boolean isIntersects) {
if (row == null) {
row = table.getTemplateRow();
} else {
ValueGeometry vg = (ValueGeometry) row.getValue(columnId);
if (isIntersects) {
v = ((ValueGeometry) v).intersection(vg);
} else {
v = ((ValueGeometry) v).union(vg);
}
}
if (columnId < 0) {
row.setKey(v.getLong());
} else {
row.setValue(columnId, v);
}
return row;
}
private SearchRow getSearchRow(SearchRow row, int columnId, Value v, boolean max) {
if (row == null) {
row = table.getTemplateRow();
......
......@@ -11,7 +11,7 @@ package org.h2.index;
*/
public class IndexType {
private boolean primaryKey, persistent, unique, hash, scan;
private boolean primaryKey, persistent, unique, hash, scan, spatial;
private boolean belongsToConstraint;
/**
......@@ -52,7 +52,7 @@ public class IndexType {
* @return the index type
*/
public static IndexType createNonUnique(boolean persistent) {
return createNonUnique(persistent, false);
return createNonUnique(persistent, false, false);
}
/**
......@@ -62,10 +62,11 @@ public class IndexType {
* @param hash if a hash index should be used
* @return the index type
*/
public static IndexType createNonUnique(boolean persistent, boolean hash) {
public static IndexType createNonUnique(boolean persistent, boolean hash, boolean spatial) {
IndexType type = new IndexType();
type.persistent = persistent;
type.hash = hash;
type.spatial = spatial;
return type;
}
......@@ -110,6 +111,15 @@ public class IndexType {
return hash;
}
/**
* Is this a spatial index?
*
* @return true if it is a spatial index
*/
public boolean isSpatial() {
return spatial;
}
/**
* Is this index persistent?
*
......@@ -156,6 +166,9 @@ public class IndexType {
if (hash) {
buff.append("HASH ");
}
if (spatial) {
buff.append("SPATIAL ");
}
buff.append("INDEX");
}
return buff.toString();
......
/*
* Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.index;
import org.h2.result.SearchRow;
import org.h2.table.TableFilter;
/**
* A spatial index. Spatial indexes are used to speed up searching spatial/geometric data.
*/
public interface SpatialIndex extends Index {
/**
* Find a row or a list of rows and create a cursor to iterate over the result.
*
* @param filter the table filter (which possibly knows
* about additional conditions)
* @param intersection the geometry which values should intersect with, or null for anything
* @return the cursor to iterate over the results
*/
Cursor findByGeometry(TableFilter filter, SearchRow intersection);
}
/*
* Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License, Version
* 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html). Initial Developer: H2 Group
*/
package org.h2.index;
import java.util.List;
import org.h2.engine.Session;
import org.h2.message.DbException;
import org.h2.result.Row;
import org.h2.result.SearchRow;
import org.h2.result.SortOrder;
import org.h2.table.IndexColumn;
import org.h2.table.RegularTable;
import org.h2.table.TableFilter;
import org.h2.value.Value;
import org.h2.value.ValueGeometry;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.index.quadtree.Quadtree;
/**
* This is an in-memory index based on a R-Tree.
*/
public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
private Quadtree root;
private final RegularTable tableData;
private long rowCount;
private boolean closed;
public SpatialTreeIndex(RegularTable table, int id, String indexName, IndexColumn[] columns, IndexType indexType) {
if (indexType.isUnique()) {
throw DbException.getUnsupportedException("not unique");
}
if (columns.length > 1) {
throw DbException.getUnsupportedException("can only do one column");
}
if ((columns[0].sortType & SortOrder.DESCENDING) != 0) {
throw DbException.getUnsupportedException("cannot do descending");
}
if ((columns[0].sortType & SortOrder.NULLS_FIRST) != 0) {
throw DbException.getUnsupportedException("cannot do nulls first");
}
if ((columns[0].sortType & SortOrder.NULLS_LAST) != 0) {
throw DbException.getUnsupportedException("cannot do nulls last");
}
initBaseIndex(table, id, indexName, columns, indexType);
tableData = table;
if (!database.isStarting()) {
if (columns[0].column.getType() != Value.GEOMETRY) {
throw DbException.getUnsupportedException("spatial index on non-geometry column, "
+ columns[0].column.getCreateSQL());
}
}
root = new Quadtree();
}
@Override
public void close(Session session) {
root = null;
closed = true;
}
@Override
public void add(Session session, Row row) {
if (closed) {
throw DbException.throwInternalError();
}
root.insert(getEnvelope(row), row);
rowCount++;
}
private Envelope getEnvelope(SearchRow row) {
Value v = row.getValue(columnIds[0]);
Geometry g = ((ValueGeometry) v).getGeometry();
return g.getEnvelopeInternal();
}
@Override
public void remove(Session session, Row row) {
if (closed) {
throw DbException.throwInternalError();
}
if (!root.remove(getEnvelope(row), row)) {
throw DbException.throwInternalError("row not found");
}
rowCount--;
}
@Override
public Cursor find(TableFilter filter, SearchRow first, SearchRow last) {
return find();
}
@Override
public Cursor find(Session session, SearchRow first, SearchRow last) {
return find();
}
@SuppressWarnings("unchecked")
private Cursor find() {
// FIXME: ideally I need external iterators, but let's see if we can get
// it working first
// FIXME: in the context of a spatial index, a query that uses ">" or "<" has no real meaning, so for now just ignore
// it and return all rows
java.util.List<Row> list = root.queryAll();
return new ListCursor(list, true/*first*/);
}
@SuppressWarnings("unchecked")
@Override
public Cursor findByGeometry(TableFilter filter, SearchRow intersection) {
// FIXME: ideally I need external iterators, but let's see if we can get
// it working first
java.util.List<Row> list;
if (intersection != null) {
list = root.query(getEnvelope(intersection));
} else {
list = root.queryAll();
}
return new ListCursor(list, true/*first*/);
}
@Override
public double getCost(Session session, int[] masks, SortOrder sortOrder) {
return getCostRangeIndex(masks, tableData.getRowCountApproximation(), sortOrder);
}
@Override
public void remove(Session session) {
truncate(session);
}
@Override
public void truncate(Session session) {
root = null;
rowCount = 0;
}
@Override
public void checkRename() {
// nothing to do
}
@Override
public boolean needRebuild() {
return true;
}
@Override
public boolean canGetFirstOrLast() {
return true;
}
@Override
public Cursor findFirstOrLast(Session session, boolean first) {
if (closed) {
throw DbException.throwInternalError();
}
// FIXME: ideally I need external iterators, but let's see if we can get
// it working first
@SuppressWarnings("unchecked")
List<Row> list = root.queryAll();
return new ListCursor(list, first);
}
@Override
public long getRowCount(Session session) {
return rowCount;
}
@Override
public long getRowCountApproximation() {
return rowCount;
}
@Override
public long getDiskSpaceUsed() {
return 0;
}
private static final class ListCursor implements Cursor {
private final List<Row> rows;
private int index;
private Row current;
public ListCursor(List<Row> rows, boolean first) {
this.rows = rows;
this.index = first ? 0 : rows.size();
}
@Override
public Row get() {
return current;
}
@Override
public SearchRow getSearchRow() {
return current;
}
@Override
public boolean next() {
current = index >= rows.size() ? null : rows.get(index++);
return current != null;
}
@Override
public boolean previous() {
current = index < 0 ? null : rows.get(index--);
return current != null;
}
}
}
......@@ -31,6 +31,7 @@ import org.h2.index.PageBtreeIndex;
import org.h2.index.PageDataIndex;
import org.h2.index.PageDelegateIndex;
import org.h2.index.ScanIndex;
import org.h2.index.SpatialTreeIndex;
import org.h2.index.TreeIndex;
import org.h2.message.DbException;
import org.h2.message.Trace;
......@@ -227,8 +228,10 @@ public class RegularTable extends TableBase {
if (mainIndexColumn != -1) {
mainIndex.setMainIndexColumn(mainIndexColumn);
index = new PageDelegateIndex(this, indexId, indexName, indexType, mainIndex, create, session);
} else {
} else if (!indexType.isSpatial()) {
index = new PageBtreeIndex(this, indexId, indexName, cols, indexType, create, session);
} else {
throw new UnsupportedOperationException();
}
} else {
if (indexType.isHash() && cols.length <= 1) {
......@@ -237,8 +240,10 @@ public class RegularTable extends TableBase {
} else {
index = new NonUniqueHashIndex(this, indexId, indexName, cols, indexType);
}
} else {
} else if (!indexType.isSpatial()) {
index = new TreeIndex(this, indexId, indexName, cols, indexType);
} else {
index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType);
}
}
if (database.isMultiVersion()) {
......
......@@ -9,6 +9,7 @@ package org.h2.value;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.h2.message.DbException;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKBReader;
import com.vividsolutions.jts.io.WKBWriter;
......@@ -23,9 +24,9 @@ public class ValueGeometry extends Value {
/**
* The value.
*/
private final com.vividsolutions.jts.geom.Geometry geometry;
private final Geometry geometry;
private ValueGeometry(com.vividsolutions.jts.geom.Geometry geometry) {
private ValueGeometry(Geometry geometry) {
this.geometry = geometry;
}
......@@ -35,7 +36,7 @@ public class ValueGeometry extends Value {
* @param g the geometry
* @return the value
*/
public static ValueGeometry get(com.vividsolutions.jts.geom.Geometry g) {
public static ValueGeometry get(Geometry g) {
return (ValueGeometry) Value.cache(new ValueGeometry(g));
}
......@@ -59,6 +60,22 @@ public class ValueGeometry extends Value {
return (ValueGeometry) Value.cache(new ValueGeometry(fromWKB(bytes)));
}
public Geometry getGeometry() {
return geometry;
}
public boolean intersects(ValueGeometry r) {
return geometry.intersects(r.getGeometry());
}
public Value intersection(ValueGeometry r) {
return get(this.geometry.intersection(r.geometry));
}
public Value union(ValueGeometry r) {
return get(this.geometry.union(r.geometry));
}
@Override
public int getType() {
return Value.GEOMETRY;
......@@ -71,7 +88,7 @@ public class ValueGeometry extends Value {
@Override
protected int compareSecure(Value v, CompareMode mode) {
com.vividsolutions.jts.geom.Geometry g = ((ValueGeometry) v).geometry;
Geometry g = ((ValueGeometry) v).geometry;
return this.geometry.compareTo(g);
}
......@@ -139,7 +156,7 @@ public class ValueGeometry extends Value {
/**
* Convert from Well-Known-Text format.
*/
private static com.vividsolutions.jts.geom.Geometry fromWKT(String s) {
private static Geometry fromWKT(String s) {
WKTReader r = new WKTReader();
try {
return r.read(s);
......@@ -151,7 +168,7 @@ public class ValueGeometry extends Value {
/**
* Convert from Well-Known-Binary format.
*/
private static com.vividsolutions.jts.geom.Geometry fromWKB(byte[] bytes) {
private static Geometry fromWKB(byte[] bytes) {
WKBReader r = new WKBReader();
try {
return r.read(bytes);
......
......@@ -32,6 +32,7 @@ public class TestSpatial extends TestBase {
public void test() throws SQLException {
deleteDb("spatial");
testSpatialValues();
testMemorySpatialIndex();
deleteDb("spatial");
}
......@@ -61,4 +62,38 @@ public class TestSpatial extends TestBase {
deleteDb("spatial");
}
/** test in the in-memory spatial index */
private void testMemorySpatialIndex() throws SQLException {
deleteDb("spatialIndex");
Connection conn = getConnection("spatialIndex");
Statement stat = conn.createStatement();
stat.execute("create memory table test(id int primary key, poly geometry)");
stat.execute("create spatial index idx_test_poly on test(poly)");
stat.execute("insert into test values(1, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
ResultSet rs = stat.executeQuery("explain select * from test where poly = 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
rs.next();
assertContains(rs.getString(1), "/* PUBLIC.IDX_TEST_POLY: POLY =");
// these queries actually have no meaning in the context of a spatial index, but
// check them anyhow
stat.executeQuery("select * from test where poly = 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
stat.executeQuery("select * from test where poly > 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
stat.executeQuery("select * from test where poly < 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
rs = stat.executeQuery("select * from test where intersects(poly, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
assertTrue(rs.next());
rs = stat.executeQuery("select * from test where intersects(poly, 'POINT (1 1)')");
assertTrue(rs.next());
rs = stat.executeQuery("select * from test where intersects(poly, 'POINT (0 0)')");
assertFalse(rs.next());
stat.execute("drop table test");
conn.close();
deleteDb("spatialIndex");
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论