Skip to content

Commit

Permalink
SQL: Add basic support for ST_AsWKT geo function (#34205)
Browse files Browse the repository at this point in the history
Adds basic support for ST_AsWKT function. The function takes accepts
geo shape or geo point and returns its WKT representation.
  • Loading branch information
imotov authored Oct 23, 2018
1 parent 0301e6b commit 0280428
Show file tree
Hide file tree
Showing 23 changed files with 609 additions and 78 deletions.
32 changes: 32 additions & 0 deletions docs/reference/sql/functions/geo.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[role="xpack"]
[testenv="basic"]
[[sql-functions-geo]]
=== Geo Functions

The geo functions work with geometries stored in `geo_point` and `geo_shape` fields, or returned by other geo functions.

==== Geometry Conversion

[[sql-functions-geo-st-as-wkt]]
===== `ST_AsWKT`

.Synopsis:
[source, sql]
--------------------------------------------------
ST_AsWKT(geometry<1>)
--------------------------------------------------

*Input*:

<1> geometry

*Output*: string

.Description:

Returns the WKT representation of the `geometry`. The return type is string.

["source","sql",subs="attributes,macros"]
--------------------------------------------------
include-tagged::{sql-specs}/geo/docs.csv-spec[aswkt]
--------------------------------------------------
1 change: 1 addition & 0 deletions docs/reference/sql/functions/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* <<sql-functions-math, Mathematical>>
* <<sql-functions-string, String>>
* <<sql-functions-type-conversion,Type Conversion>>
* <<sql-functions-geo,Geo>>

include::operators.asciidoc[]
include::aggs.asciidoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@

import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.mapper.GeoShapeFieldMapper;

import java.io.IOException;
import java.io.InputStream;

/**
* first point of entry for a shape parser
Expand Down Expand Up @@ -67,4 +73,20 @@ static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper
static ShapeBuilder parse(XContentParser parser) throws IOException {
return parse(parser, null);
}

static ShapeBuilder parse(Object value) throws IOException {
XContentBuilder content = JsonXContent.contentBuilder();
content.startObject();
content.field("value", value);
content.endObject();

try (InputStream stream = BytesReference.bytes(content).streamInput();
XContentParser parser = JsonXContent.jsonXContent.createParser(
NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
parser.nextToken(); // start object
parser.nextToken(); // field name
parser.nextToken(); // field value
return parse(parser);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public enum DataType {
public String sqlName() {
return jdbcType.getName();
}

public Class<?> javaClass() {
return javaClass;
}
Expand All @@ -152,13 +152,18 @@ public boolean isPrimitive() {
return this != OBJECT && this != NESTED;
}


public boolean isGeo() {
return this == GEO_POINT || this == GEO_SHAPE;
}

public static DataType fromJdbcType(SQLType jdbcType) {
if (jdbcToEs.containsKey(jdbcType) == false) {
throw new IllegalArgumentException("Unsupported JDBC type [" + jdbcType + "]");
}
return jdbcToEs.get(jdbcType);
}

public static Class<?> fromJdbcTypeToJava(SQLType jdbcType) {
if (jdbcToEs.containsKey(jdbcType) == false) {
throw new IllegalArgumentException("Unsupported JDBC type [" + jdbcType + "]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
*/
package org.elasticsearch.xpack.sql.execution.search.extractor;

import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.Version;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoShape;
import org.elasticsearch.xpack.sql.type.DataType;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
Expand Down Expand Up @@ -121,6 +124,21 @@ private Object unwrapMultiValue(Object values) {
if (values == null) {
return null;
}
if (dataType == DataType.GEO_POINT) {
try {
return GeoUtils.parseGeoPoint(values, true);
} catch (ElasticsearchParseException ex) {
throw new SqlIllegalArgumentException("Cannot parse geo_point value (returned by [{}])", fieldName);
}

}
if (dataType == DataType.GEO_SHAPE) {
try {
return new GeoShape(values);
} catch (IOException ex) {
throw new SqlIllegalArgumentException("Cannot read geo_shape value (returned by [{}])", fieldName);
}
}
if (values instanceof List) {
List<?> list = (List<?>) values;
if (list.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.SecondOfMinute;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.WeekOfYear;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Year;
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StAswkt;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ACos;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ASin;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ATan;
Expand Down Expand Up @@ -116,14 +117,14 @@ public class FunctionRegistry {
public FunctionRegistry() {
defineDefaultFunctions();
}

/**
* Constructor specifying alternate functions for testing.
*/
FunctionRegistry(FunctionDefinition... functions) {
addToMap(functions);
}

private void defineDefaultFunctions() {
// Aggregate functions
addToMap(def(Avg.class, Avg::new),
Expand Down Expand Up @@ -206,10 +207,12 @@ private void defineDefaultFunctions() {
def(Space.class, Space::new),
def(Substring.class, Substring::new),
def(UCase.class, UCase::new));
// Geo Functions
addToMap(def(StAswkt.class, StAswkt::new));
// Special
addToMap(def(Score.class, Score::new));
}

protected void addToMap(FunctionDefinition...functions) {
// temporary map to hold [function_name/alias_name : function instance]
Map<String, FunctionDefinition> batchMap = new HashMap<>();
Expand All @@ -227,7 +230,7 @@ protected void addToMap(FunctionDefinition...functions) {
// sort the temporary map by key name and add it to the global map of functions
defs.putAll(batchMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.<Entry<String, FunctionDefinition>, String,
.collect(Collectors.<Entry<String, FunctionDefinition>, String,
FunctionDefinition, LinkedHashMap<String, FunctionDefinition>> toMap(Map.Entry::getKey, Map.Entry::getValue,
(oldValue, newValue) -> oldValue, LinkedHashMap::new)));
}
Expand Down Expand Up @@ -390,7 +393,7 @@ private static FunctionDefinition def(Class<? extends Function> function, Functi
private interface FunctionBuilder {
Function build(Location location, List<Expression> children, boolean distinct, TimeZone tz);
}

@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
static <T extends Function> FunctionDefinition def(Class<T> function,
ThreeParametersFunctionBuilder<T> ctorRef, String... aliases) {
Expand All @@ -408,11 +411,11 @@ static <T extends Function> FunctionDefinition def(Class<T> function,
};
return def(function, builder, false, aliases);
}

interface ThreeParametersFunctionBuilder<T> {
T build(Location location, Expression source, Expression exp1, Expression exp2);
}

@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
static <T extends Function> FunctionDefinition def(Class<T> function,
FourParametersFunctionBuilder<T> ctorRef, String... aliases) {
Expand All @@ -427,7 +430,7 @@ static <T extends Function> FunctionDefinition def(Class<T> function,
};
return def(function, builder, false, aliases);
}

interface FourParametersFunctionBuilder<T> {
T build(Location location, Expression source, Expression exp1, Expression exp2, Expression exp3);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor;
import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor;
Expand Down Expand Up @@ -71,6 +72,7 @@ public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
entries.add(new Entry(Processor.class, LocateFunctionProcessor.NAME, LocateFunctionProcessor::new));
entries.add(new Entry(Processor.class, ReplaceFunctionProcessor.NAME, ReplaceFunctionProcessor::new));
entries.add(new Entry(Processor.class, SubstringFunctionProcessor.NAME, SubstringFunctionProcessor::new));
entries.add(new Entry(Processor.class, GeoProcessor.NAME, GeoProcessor::new));
return entries;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function.scalar.geo;

import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.builders.PointBuilder;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.gen.processor.Processor;

import java.io.IOException;
import java.util.function.Function;

public class GeoProcessor implements Processor {

private interface GeoPointFunction<R> {
default R apply(Object o) {
if (!(o instanceof GeoPoint)) {
throw new SqlIllegalArgumentException("A geo_point is required; received [{}]", o);
}
return doApply((GeoPoint) o);
}

R doApply(GeoPoint s);
}


private interface GeoShapeFunction<R> {
default R apply(Object o) {
if (!(o instanceof GeoShape)) {
throw new SqlIllegalArgumentException("A geo_shape is required; received [{}]", o);
}

return doApply((GeoShape) o);
}

R doApply(GeoShape s);
}

public enum GeoOperation {
ASWKT_POINT((GeoPoint p) -> new PointBuilder(p.getLon(), p.getLat()).toWKT()),
ASWKT_SHAPE(GeoShape::toString);

private final Function<Object, Object> apply;

GeoOperation(GeoPointFunction<Object> apply) {
this.apply = l -> l == null ? null : apply.apply(l);
}

GeoOperation(GeoShapeFunction<Object> apply) {
this.apply = l -> l == null ? null : apply.apply(l);
}

public final Object apply(Object l) {
return apply.apply(l);
}
}

public static final String NAME = "geo";

private final GeoOperation processor;

public GeoProcessor(GeoOperation processor) {
this.processor = processor;
}

public GeoProcessor(StreamInput in) throws IOException {
processor = in.readEnum(GeoOperation.class);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeEnum(processor);
}

@Override
public String getWriteableName() {
return NAME;
}

@Override
public Object process(Object input) {
return processor.apply(input);
}

GeoOperation processor() {
return processor;
}

@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
GeoProcessor other = (GeoProcessor) obj;
return processor == other.processor;
}

@Override
public int hashCode() {
return processor.hashCode();
}

@Override
public String toString() {
return processor.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function.scalar.geo;

import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.geo.parsers.ShapeParser;
import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;

/**
* Wrapper class to represent a GeoShape in SQL
*
* It is required to override the XContent serialization. The ShapeBuilder serializes using GeoJSON by default,
* but in SQL we need the serialization to be WKT-based.
*/
public class GeoShape implements ToXContentFragment {

private final ShapeBuilder<?, ?> shapeBuilder;

public GeoShape(Object value) throws IOException {
shapeBuilder = ShapeParser.parse(value);
}

@Override
public String toString() {
return shapeBuilder.toWKT();
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(shapeBuilder.toWKT());
}
}
Loading

0 comments on commit 0280428

Please sign in to comment.