Merge pull request #2611 from dolthub/james/spatial-types

Spatial types actually stored as spatial types
This commit is contained in:
James Cor
2022-01-27 10:25:49 -08:00
committed by GitHub
30 changed files with 1585 additions and 10 deletions

View File

@@ -214,6 +214,12 @@ func bitTypeConverter(ctx context.Context, src *bitType, destTi TypeInfo) (tc Ty
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapIsValid(dest.IsValid, src, dest)
case *timeType:

View File

@@ -228,6 +228,12 @@ func blobStringTypeConverter(ctx context.Context, src *blobStringType, destTi Ty
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -231,6 +231,12 @@ func boolTypeConverter(ctx context.Context, src *boolType, destTi TypeInfo) (tc
}, true, nil
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *intType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:

View File

@@ -225,6 +225,12 @@ func datetimeTypeConverter(ctx context.Context, src *datetimeType, destTi TypeIn
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -259,6 +259,12 @@ func decimalTypeConverter(ctx context.Context, src *decimalType, destTi TypeInfo
}, true, nil
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
s, err := src.ConvertNomsValueToValue(v)

View File

@@ -247,6 +247,12 @@ func enumTypeConverter(ctx context.Context, src *enumType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -255,6 +255,12 @@ func floatTypeConverter(ctx context.Context, src *floatType, destTi TypeInfo) (t
return floatTypeConverterRoundToZero(ctx, src, destTi)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
if v == nil || v == types.NullValue {

View File

@@ -241,6 +241,12 @@ func inlineBlobTypeConverter(ctx context.Context, src *inlineBlobType, destTi Ty
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -283,6 +283,12 @@ func intTypeConverter(ctx context.Context, src *intType, destTi TypeInfo) (tc Ty
return wrapIsValid(dest.IsValid, src, dest)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -173,6 +173,12 @@ func jsonTypeConverter(ctx context.Context, src *jsonType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapIsValid(dest.IsValid, src, dest)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -0,0 +1,213 @@
// Copyright 2020 Dolthub, Inc.
//
// Licensed 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 typeinfo
import (
"context"
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/store/types"
)
// This is a dolt implementation of the MySQL type Point, thus most of the functionality
// within is directly reliant on the go-mysql-server implementation.
type linestringType struct {
sqlLinestringType sql.LinestringType
}
var _ TypeInfo = (*linestringType)(nil)
var LinestringType = &linestringType{sql.LinestringType{}}
// ConvertTypesLinestringToSQLLinestring basically makes a deep copy of sql.Linestring
func ConvertTypesLinestringToSQLLinestring(l types.Linestring) sql.Linestring {
points := make([]sql.Point, len(l.Points))
for i, p := range l.Points {
points[i] = ConvertTypesPointToSQLPoint(p)
}
return sql.Linestring{SRID: l.SRID, Points: points}
}
// ConvertNomsValueToValue implements TypeInfo interface.
func (ti *linestringType) ConvertNomsValueToValue(v types.Value) (interface{}, error) {
// Expect a types.Linestring, return a sql.Linestring
if val, ok := v.(types.Linestring); ok {
return ConvertTypesLinestringToSQLLinestring(val), nil
}
// Check for null
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), v.Kind())
}
// ReadFrom reads a go value from a noms types.CodecReader directly
func (ti *linestringType) ReadFrom(nbf *types.NomsBinFormat, reader types.CodecReader) (interface{}, error) {
k := reader.ReadKind()
switch k {
case types.LinestringKind:
l, err := reader.ReadLinestring()
if err != nil {
return nil, err
}
return ti.ConvertNomsValueToValue(l)
case types.NullKind:
return nil, nil
}
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), k)
}
func ConvertSQLLinestringToTypesLinestring(l sql.Linestring) types.Linestring {
points := make([]types.Point, len(l.Points))
for i, p := range l.Points {
points[i] = ConvertSQLPointToTypesPoint(p)
}
return types.Linestring{SRID: l.SRID, Points: points}
}
// ConvertValueToNomsValue implements TypeInfo interface.
func (ti *linestringType) ConvertValueToNomsValue(ctx context.Context, vrw types.ValueReadWriter, v interface{}) (types.Value, error) {
// Check for null
if v == nil {
return types.NullValue, nil
}
// Convert to sql.LinestringType
line, err := ti.sqlLinestringType.Convert(v)
if err != nil {
return nil, err
}
return ConvertSQLLinestringToTypesLinestring(line.(sql.Linestring)), nil
}
// Equals implements TypeInfo interface.
func (ti *linestringType) Equals(other TypeInfo) bool {
if other == nil {
return false
}
_, ok := other.(*linestringType)
return ok
}
// FormatValue implements TypeInfo interface.
func (ti *linestringType) FormatValue(v types.Value) (*string, error) {
if val, ok := v.(types.Linestring); ok {
buf := make([]byte, types.EWKBHeaderSize+types.LengthSize+types.PointDataSize*len(val.Points))
types.WriteEWKBHeader(val, buf[:types.EWKBHeaderSize])
types.WriteEWKBLineData(val, buf[types.EWKBHeaderSize:])
resStr := string(buf)
return &resStr, nil
}
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" has unexpectedly encountered a value of type "%T" from embedded type`, ti.String(), v.Kind())
}
// GetTypeIdentifier implements TypeInfo interface.
func (ti *linestringType) GetTypeIdentifier() Identifier {
return LinestringTypeIdentifier
}
// GetTypeParams implements TypeInfo interface.
func (ti *linestringType) GetTypeParams() map[string]string {
return map[string]string{}
}
// IsValid implements TypeInfo interface.
func (ti *linestringType) IsValid(v types.Value) bool {
if _, ok := v.(types.Linestring); ok {
return true
}
if _, ok := v.(types.Null); ok || v == nil {
return true
}
return false
}
// NomsKind implements TypeInfo interface.
func (ti *linestringType) NomsKind() types.NomsKind {
return types.LinestringKind
}
// Promote implements TypeInfo interface.
func (ti *linestringType) Promote() TypeInfo {
return &linestringType{ti.sqlLinestringType.Promote().(sql.LinestringType)}
}
// String implements TypeInfo interface.
func (ti *linestringType) String() string {
return "Linestring"
}
// ToSqlType implements TypeInfo interface.
func (ti *linestringType) ToSqlType() sql.Type {
return ti.sqlLinestringType
}
// linestringTypeConverter is an internal function for GetTypeConverter that handles the specific type as the source TypeInfo.
func linestringTypeConverter(ctx context.Context, src *linestringType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
switch dest := destTi.(type) {
case *bitType:
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
return types.Uint(0), nil
}, true, nil
case *blobStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *boolType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *datetimeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *decimalType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *enumType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *floatType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *inlineBlobType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *intType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return identityTypeConverter, false, nil
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uintType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uuidType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varBinaryType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *yearType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
default:
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
}
}

View File

@@ -0,0 +1,207 @@
// Copyright 2020 Dolthub, Inc.
//
// Licensed 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 typeinfo
import (
"context"
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/store/types"
)
// This is a dolt implementation of the MySQL type Point, thus most of the functionality
// within is directly reliant on the go-mysql-server implementation.
type pointType struct {
sqlPointType sql.PointType
}
var _ TypeInfo = (*pointType)(nil)
var PointType = &pointType{sql.PointType{}}
// ConvertTypesPointToSQLPoint basically makes a deep copy of sql.Point
func ConvertTypesPointToSQLPoint(p types.Point) sql.Point {
return sql.Point{SRID: p.SRID, X: p.X, Y: p.Y}
}
// ConvertNomsValueToValue implements TypeInfo interface.
func (ti *pointType) ConvertNomsValueToValue(v types.Value) (interface{}, error) {
// Expect a types.Point, return a sql.Point
if val, ok := v.(types.Point); ok {
return ConvertTypesPointToSQLPoint(val), nil
}
// Check for null
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), v.Kind())
}
// ReadFrom reads a go value from a noms types.CodecReader directly
func (ti *pointType) ReadFrom(nbf *types.NomsBinFormat, reader types.CodecReader) (interface{}, error) {
k := reader.ReadKind()
switch k {
case types.PointKind:
p, err := reader.ReadPoint()
if err != nil {
return nil, err
}
return ti.ConvertNomsValueToValue(p)
case types.NullKind:
return nil, nil
default:
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), k)
}
}
// TODO: define constants for WKB?
func ConvertSQLPointToTypesPoint(p sql.Point) types.Point {
return types.Point{SRID: p.SRID, X: p.X, Y: p.Y}
}
// ConvertValueToNomsValue implements TypeInfo interface.
func (ti *pointType) ConvertValueToNomsValue(ctx context.Context, vrw types.ValueReadWriter, v interface{}) (types.Value, error) {
// Check for null
if v == nil {
return types.NullValue, nil
}
// Convert to sql.PointType
point, err := ti.sqlPointType.Convert(v)
if err != nil {
return nil, err
}
return ConvertSQLPointToTypesPoint(point.(sql.Point)), nil
}
// Equals implements TypeInfo interface.
func (ti *pointType) Equals(other TypeInfo) bool {
if other == nil {
return false
}
_, ok := other.(*pointType)
return ok
}
// FormatValue implements TypeInfo interface.
func (ti *pointType) FormatValue(v types.Value) (*string, error) {
if val, ok := v.(types.Point); ok {
buf := make([]byte, types.EWKBHeaderSize+types.PointDataSize)
types.WriteEWKBHeader(val, buf[:types.EWKBHeaderSize])
types.WriteEWKBPointData(val, buf[types.EWKBHeaderSize:])
resStr := string(buf)
return &resStr, nil
}
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" has unexpectedly encountered a value of type "%T" from embedded type`, ti.String(), v.Kind())
}
// GetTypeIdentifier implements TypeInfo interface.
func (ti *pointType) GetTypeIdentifier() Identifier {
return PointTypeIdentifier
}
// GetTypeParams implements TypeInfo interface.
func (ti *pointType) GetTypeParams() map[string]string {
return map[string]string{}
}
// IsValid implements TypeInfo interface.
func (ti *pointType) IsValid(v types.Value) bool {
if _, ok := v.(types.Point); ok {
return true
}
if _, ok := v.(types.Null); ok || v == nil {
return true
}
return false
}
// NomsKind implements TypeInfo interface.
func (ti *pointType) NomsKind() types.NomsKind {
return types.PointKind
}
// Promote implements TypeInfo interface.
func (ti *pointType) Promote() TypeInfo {
return &pointType{ti.sqlPointType.Promote().(sql.PointType)}
}
// String implements TypeInfo interface.
func (ti *pointType) String() string {
return "Point"
}
// ToSqlType implements TypeInfo interface.
func (ti *pointType) ToSqlType() sql.Type {
return ti.sqlPointType
}
// pointTypeConverter is an internal function for GetTypeConverter that handles the specific type as the source TypeInfo.
func pointTypeConverter(ctx context.Context, src *pointType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
switch dest := destTi.(type) {
case *bitType:
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
return types.Uint(0), nil
}, true, nil
case *blobStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *boolType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *datetimeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *decimalType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *enumType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *floatType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *inlineBlobType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *intType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return identityTypeConverter, false, nil
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uintType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uuidType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varBinaryType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *yearType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
default:
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
}
}

View File

@@ -0,0 +1,217 @@
// Copyright 2020 Dolthub, Inc.
//
// Licensed 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 typeinfo
import (
"context"
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/store/types"
)
// This is a dolt implementation of the MySQL type Point, thus most of the functionality
// within is directly reliant on the go-mysql-server implementation.
type polygonType struct {
sqlPolygonType sql.PolygonType
}
var _ TypeInfo = (*polygonType)(nil)
var PolygonType = &polygonType{sql.PolygonType{}}
// ConvertTypesPolygonToSQLPolygon basically makes a deep copy of sql.Linestring
func ConvertTypesPolygonToSQLPolygon(p types.Polygon) sql.Polygon {
lines := make([]sql.Linestring, len(p.Lines))
for i, l := range p.Lines {
lines[i] = ConvertTypesLinestringToSQLLinestring(l)
}
return sql.Polygon{SRID: p.SRID, Lines: lines}
}
// ConvertNomsValueToValue implements TypeInfo interface.
func (ti *polygonType) ConvertNomsValueToValue(v types.Value) (interface{}, error) {
// Expect a types.Polygon, return a sql.Polygon
if val, ok := v.(types.Polygon); ok {
return ConvertTypesPolygonToSQLPolygon(val), nil
}
// Check for null
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), v.Kind())
}
// ReadFrom reads a go value from a noms types.CodecReader directly
func (ti *polygonType) ReadFrom(nbf *types.NomsBinFormat, reader types.CodecReader) (interface{}, error) {
k := reader.ReadKind()
switch k {
case types.PolygonKind:
p, err := reader.ReadPolygon()
if err != nil {
return nil, err
}
return ti.ConvertNomsValueToValue(p)
case types.NullKind:
return nil, nil
}
return nil, fmt.Errorf(`"%v" cannot convert NomsKind "%v" to a value`, ti.String(), k)
}
func ConvertSQLPolygonToTypesPolygon(p sql.Polygon) types.Polygon {
lines := make([]types.Linestring, len(p.Lines))
for i, l := range p.Lines {
lines[i] = ConvertSQLLinestringToTypesLinestring(l)
}
return types.Polygon{SRID: p.SRID, Lines: lines}
}
// ConvertValueToNomsValue implements TypeInfo interface.
func (ti *polygonType) ConvertValueToNomsValue(ctx context.Context, vrw types.ValueReadWriter, v interface{}) (types.Value, error) {
// Check for null
if v == nil {
return types.NullValue, nil
}
// Convert to sql.PolygonType
poly, err := ti.sqlPolygonType.Convert(v)
if err != nil {
return nil, err
}
return ConvertSQLPolygonToTypesPolygon(poly.(sql.Polygon)), nil
}
// Equals implements TypeInfo interface.
func (ti *polygonType) Equals(other TypeInfo) bool {
if other == nil {
return false
}
_, ok := other.(*polygonType)
return ok
}
// FormatValue implements TypeInfo interface.
func (ti *polygonType) FormatValue(v types.Value) (*string, error) {
if val, ok := v.(types.Polygon); ok {
size := types.EWKBHeaderSize + types.LengthSize
for _, l := range val.Lines {
size += types.LengthSize + types.PointDataSize*len(l.Points)
}
buf := make([]byte, size)
types.WriteEWKBHeader(val, buf[:types.EWKBHeaderSize])
types.WriteEWKBPolyData(val, buf[types.EWKBHeaderSize:])
resStr := string(buf)
return &resStr, nil
}
if _, ok := v.(types.Null); ok || v == nil {
return nil, nil
}
return nil, fmt.Errorf(`"%v" has unexpectedly encountered a value of type "%T" from embedded type`, ti.String(), v.Kind())
}
// GetTypeIdentifier implements TypeInfo interface.
func (ti *polygonType) GetTypeIdentifier() Identifier {
return PolygonTypeIdentifier
}
// GetTypeParams implements TypeInfo interface.
func (ti *polygonType) GetTypeParams() map[string]string {
return map[string]string{}
}
// IsValid implements TypeInfo interface.
func (ti *polygonType) IsValid(v types.Value) bool {
if _, ok := v.(types.Polygon); ok {
return true
}
if _, ok := v.(types.Null); ok || v == nil {
return true
}
return false
}
// NomsKind implements TypeInfo interface.
func (ti *polygonType) NomsKind() types.NomsKind {
return types.PolygonKind
}
// Promote implements TypeInfo interface.
func (ti *polygonType) Promote() TypeInfo {
return &polygonType{ti.sqlPolygonType.Promote().(sql.PolygonType)}
}
// String implements TypeInfo interface.
func (ti *polygonType) String() string {
return "Polygon"
}
// ToSqlType implements TypeInfo interface.
func (ti *polygonType) ToSqlType() sql.Type {
return ti.sqlPolygonType
}
// polygonTypeConverter is an internal function for GetTypeConverter that handles the specific type as the source TypeInfo.
func polygonTypeConverter(ctx context.Context, src *polygonType, destTi TypeInfo) (tc TypeConverter, needsConversion bool, err error) {
switch dest := destTi.(type) {
case *bitType:
return func(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (types.Value, error) {
return types.Uint(0), nil
}, true, nil
case *blobStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *boolType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *datetimeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *decimalType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *enumType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *floatType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *inlineBlobType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *intType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return identityTypeConverter, false, nil
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uintType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *uuidType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varBinaryType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *varStringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *yearType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
default:
return nil, false, UnhandledTypeConversion.New(src.String(), destTi.String())
}
}

View File

@@ -223,6 +223,12 @@ func setTypeConverter(ctx context.Context, src *setType, destTi TypeInfo) (tc Ty
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapIsValid(dest.IsValid, src, dest)
case *timeType:

View File

@@ -159,6 +159,12 @@ func timeTypeConverter(ctx context.Context, src *timeType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -71,6 +71,12 @@ func GetTypeConverter(ctx context.Context, srcTi TypeInfo, destTi TypeInfo) (tc
return intTypeConverter(ctx, src, destTi)
case *jsonType:
return jsonTypeConverter(ctx, src, destTi)
case *linestringType:
return linestringTypeConverter(ctx, src, destTi)
case *pointType:
return pointTypeConverter(ctx, src, destTi)
case *polygonType:
return polygonTypeConverter(ctx, src, destTi)
case *setType:
return setTypeConverter(ctx, src, destTi)
case *timeType:
@@ -131,6 +137,12 @@ func wrapConvertValueToNomsValue(
if err != nil {
return nil, err
}
case types.Linestring:
vInt = ConvertTypesLinestringToSQLLinestring(val)
case types.Point:
vInt = ConvertTypesPointToSQLPoint(val)
case types.Polygon:
vInt = ConvertTypesPolygonToSQLPolygon(val)
case types.String:
vInt = string(val)
case types.Timestamp:

View File

@@ -18,6 +18,8 @@ import (
"context"
"fmt"
"math"
"os"
"sync"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/vitess/go/sqltypes"
@@ -25,6 +27,33 @@ import (
"github.com/dolthub/dolt/go/store/types"
)
const spatialTypesFeatureFlagKey = "DOLT_ENABLE_SPATIAL_TYPES"
// use SpatialTypesEnabled() to check, don't access directly
var spatialTypesFeatureFlag = false
func init() {
// set the spatial types feature flag to true if the env var is set
if v, ok := os.LookupEnv(spatialTypesFeatureFlagKey); ok && v != "" {
spatialTypesFeatureFlag = true
}
}
var spatialTypesLock = &sync.RWMutex{}
func SpatialTypesEnabled() bool {
return spatialTypesFeatureFlag
}
func TestWithSpatialTypesEnabled(cb func()) {
spatialTypesLock.Lock()
defer spatialTypesLock.Unlock()
spatialTypesFeatureFlag = true
cb()
spatialTypesFeatureFlag = false
}
type Identifier string
const (
@@ -47,6 +76,9 @@ const (
VarBinaryTypeIdentifier Identifier = "varbinary"
VarStringTypeIdentifier Identifier = "varstring"
YearTypeIdentifier Identifier = "year"
PointTypeIdentifier Identifier = "point"
LinestringTypeIdentifier Identifier = "linestring"
PolygonTypeIdentifier Identifier = "polygon"
)
var Identifiers = map[Identifier]struct{}{
@@ -69,6 +101,9 @@ var Identifiers = map[Identifier]struct{}{
VarBinaryTypeIdentifier: {},
VarStringTypeIdentifier: {},
YearTypeIdentifier: {},
PointTypeIdentifier: {},
LinestringTypeIdentifier: {},
PolygonTypeIdentifier: {},
}
// TypeInfo is an interface used for encoding type information.
@@ -155,6 +190,18 @@ func FromSqlType(sqlType sql.Type) (TypeInfo, error) {
return DatetimeType, nil
case sqltypes.Year:
return YearType, nil
case sqltypes.Geometry:
// TODO: bad, but working way to determine which specific geometry type
switch sqlType.String() {
case sql.PolygonType{}.String():
return &polygonType{sqlType.(sql.PolygonType)}, nil
case sql.LinestringType{}.String():
return &linestringType{sqlType.(sql.LinestringType)}, nil
case sql.PointType{}.String():
return &pointType{sqlType.(sql.PointType)}, nil
default:
return nil, fmt.Errorf(`expected "PointTypeIdentifier" from SQL basetype "Geometry"`)
}
case sqltypes.Decimal:
decimalSQLType, ok := sqlType.(sql.DecimalType)
if !ok {
@@ -228,6 +275,17 @@ func FromSqlType(sqlType sql.Type) (TypeInfo, error) {
// FromTypeParams constructs a TypeInfo from the given identifier and parameters.
func FromTypeParams(id Identifier, params map[string]string) (TypeInfo, error) {
if SpatialTypesEnabled() {
switch id {
case PointTypeIdentifier:
return PointType, nil
case LinestringTypeIdentifier:
return LinestringType, nil
case PolygonTypeIdentifier:
return PolygonType, nil
}
}
switch id {
case BitTypeIdentifier:
return CreateBitTypeFromParams(params)
@@ -285,8 +343,14 @@ func FromKind(kind types.NomsKind) TypeInfo {
return Int64Type
case types.JSONKind:
return JSONType
case types.LinestringKind:
return LinestringType
case types.NullKind:
return UnknownType
case types.PointKind:
return PointType
case types.PolygonKind:
return PolygonType
case types.StringKind:
return StringDefaultType
case types.TimestampKind:

View File

@@ -221,11 +221,23 @@ func testTypeInfoGetTypeParams(t *testing.T, tiArrays [][]TypeInfo) {
for _, tiArray := range tiArrays {
t.Run(tiArray[0].GetTypeIdentifier().String(), func(t *testing.T) {
for _, ti := range tiArray {
t.Run(ti.String(), func(t *testing.T) {
newTi, err := FromTypeParams(ti.GetTypeIdentifier(), ti.GetTypeParams())
require.NoError(t, err)
require.True(t, ti.Equals(newTi), "%v\n%v", ti.String(), newTi.String())
})
if ti.GetTypeIdentifier() == PointTypeIdentifier ||
ti.GetTypeIdentifier() == LinestringTypeIdentifier ||
ti.GetTypeIdentifier() == PolygonTypeIdentifier {
t.Run(ti.String(), func(t *testing.T) {
TestWithSpatialTypesEnabled(func() {
newTi, err := FromTypeParams(ti.GetTypeIdentifier(), ti.GetTypeParams())
require.NoError(t, err)
require.True(t, ti.Equals(newTi), "%v\n%v", ti.String(), newTi.String())
})
})
} else {
t.Run(ti.String(), func(t *testing.T) {
newTi, err := FromTypeParams(ti.GetTypeIdentifier(), ti.GetTypeParams())
require.NoError(t, err)
require.True(t, ti.Equals(newTi), "%v\n%v", ti.String(), newTi.String())
})
}
}
})
}
@@ -330,6 +342,9 @@ func generateTypeInfoArrays(t *testing.T) ([][]TypeInfo, [][]types.Value) {
{DefaultInlineBlobType},
{Int8Type, Int16Type, Int24Type, Int32Type, Int64Type},
{JSONType},
{LinestringType},
{PointType},
{PolygonType},
generateSetTypes(t, 16),
{TimeType},
{Uint8Type, Uint16Type, Uint24Type, Uint32Type, Uint64Type},
@@ -362,10 +377,13 @@ func generateTypeInfoArrays(t *testing.T) ([][]TypeInfo, [][]types.Value) {
{types.Int(20), types.Int(215), types.Int(237493), types.Int(2035753568), types.Int(2384384576063)}, //Int
{json.MustTypesJSON(`null`), json.MustTypesJSON(`[]`), json.MustTypesJSON(`"lorem ipsum"`), json.MustTypesJSON(`2.71`),
json.MustTypesJSON(`false`), json.MustTypesJSON(`{"a": 1, "b": []}`)}, //JSON
{types.Uint(1), types.Uint(5), types.Uint(64), types.Uint(42), types.Uint(192)}, //Set
{types.Int(0), types.Int(1000000 /*"00:00:01"*/), types.Int(113000000 /*"00:01:53"*/), types.Int(247019000000 /*"68:36:59"*/), types.Int(458830485214 /*"127:27:10.485214"*/)}, //Time
{types.Uint(20), types.Uint(275), types.Uint(328395), types.Uint(630257298), types.Uint(93897259874)}, //Uint
{types.UUID{3}, types.UUID{3, 13}, types.UUID{128, 238, 82, 12}, types.UUID{31, 54, 23, 13, 63, 43}, types.UUID{83, 64, 21, 14, 42, 6, 35, 7, 54, 234, 6, 32, 1, 4, 2, 4}}, //Uuid
{types.Linestring{SRID: 0, Points: []types.Point{{SRID: 0, X: 1, Y: 2}, {SRID: 0, X: 3, Y: 4}}}}, // Linestring
{types.Point{SRID: 0, X: 1, Y: 2}}, // Point
{types.Polygon{SRID: 0, Lines: []types.Linestring{{SRID: 0, Points: []types.Point{{SRID: 0, X: 0, Y: 0}, {SRID: 0, X: 0, Y: 1}, {SRID: 0, X: 1, Y: 1}, {SRID: 0, X: 0, Y: 0}}}}}}, // Polygon
{types.Uint(1), types.Uint(5), types.Uint(64), types.Uint(42), types.Uint(192)}, //Set
{types.Int(0), types.Int(1000000 /*"00:00:01"*/), types.Int(113000000 /*"00:01:53"*/), types.Int(247019000000 /*"68:36:59"*/), types.Int(458830485214 /*"127:27:10.485214"*/)}, //Time
{types.Uint(20), types.Uint(275), types.Uint(328395), types.Uint(630257298), types.Uint(93897259874)}, //Uint
{types.UUID{3}, types.UUID{3, 13}, types.UUID{128, 238, 82, 12}, types.UUID{31, 54, 23, 13, 63, 43}, types.UUID{83, 64, 21, 14, 42, 6, 35, 7, 54, 234, 6, 32, 1, 4, 2, 4}}, //Uuid
{mustBlobBytes(t, []byte{1}), mustBlobBytes(t, []byte{42, 52}), mustBlobBytes(t, []byte{84, 32, 13, 63, 12, 86}), //VarBinary
mustBlobBytes(t, []byte{1, 32, 235, 64, 32, 23, 45, 76}), mustBlobBytes(t, []byte{123, 234, 34, 223, 76, 35, 32, 12, 84, 26, 15, 34, 65, 86, 45, 23, 43, 12, 76, 154, 234, 76, 34})},
{types.String(""), types.String("a"), types.String("abc"), //VarString

View File

@@ -283,6 +283,12 @@ func uintTypeConverter(ctx context.Context, src *uintType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -161,6 +161,12 @@ func uuidTypeConverter(ctx context.Context, src *uuidType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -266,6 +266,12 @@ func varBinaryTypeConverter(ctx context.Context, src *varBinaryType, destTi Type
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -297,6 +297,12 @@ func varStringTypeConverter(ctx context.Context, src *varStringType, destTi Type
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -172,6 +172,12 @@ func yearTypeConverter(ctx context.Context, src *yearType, destTi TypeInfo) (tc
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *jsonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *linestringType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *pointType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *polygonType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *setType:
return wrapConvertValueToNomsValue(dest.ConvertValueToNomsValue)
case *timeType:

View File

@@ -16,8 +16,10 @@ package sqlutil
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"strconv"
"time"
@@ -252,6 +254,56 @@ func keylessDoltRowFromSqlRow(ctx context.Context, vrw types.ValueReadWriter, sq
return row.KeylessRow(vrw.Format(), vals[:j]...)
}
// WriteEWKBHeader writes the SRID, endianness, and type to the byte buffer
// This function assumes v is a valid spatial type
func WriteEWKBHeader(v interface{}, buf []byte) {
// Write endianness byte (always little endian)
buf[4] = 1
// Parse data
switch v := v.(type) {
case sql.Point:
// Write SRID and type
binary.LittleEndian.PutUint32(buf[0:4], v.SRID)
binary.LittleEndian.PutUint32(buf[5:9], 1)
case sql.Linestring:
binary.LittleEndian.PutUint32(buf[0:4], v.SRID)
binary.LittleEndian.PutUint32(buf[5:9], 2)
case sql.Polygon:
binary.LittleEndian.PutUint32(buf[0:4], v.SRID)
binary.LittleEndian.PutUint32(buf[5:9], 3)
}
}
// WriteEWKBPointData converts a Point into a byte array in EWKB format
// Very similar to function in GMS
func WriteEWKBPointData(p sql.Point, buf []byte) {
binary.LittleEndian.PutUint64(buf[0:8], math.Float64bits(p.X))
binary.LittleEndian.PutUint64(buf[8:16], math.Float64bits(p.Y))
}
// WriteEWKBLineData converts a Line into a byte array in EWKB format
func WriteEWKBLineData(l sql.Linestring, buf []byte) {
// Write length of linestring
binary.LittleEndian.PutUint32(buf[:4], uint32(len(l.Points)))
// Append each point
for i, p := range l.Points {
WriteEWKBPointData(p, buf[4+16*i:4+16*(i+1)])
}
}
// WriteEWKBPolyData converts a Polygon into a byte array in EWKB format
func WriteEWKBPolyData(p sql.Polygon, buf []byte) {
// Write length of polygon
binary.LittleEndian.PutUint32(buf[:4], uint32(len(p.Lines)))
// Write each line
start, stop := 0, 4
for _, l := range p.Lines {
start, stop = stop, stop+4+16*len(l.Points)
WriteEWKBLineData(l, buf[start:stop])
}
}
// SqlColToStr is a utility function for converting a sql column of type interface{} to a string
func SqlColToStr(ctx context.Context, col interface{}) string {
if col != nil {
@@ -292,6 +344,25 @@ func SqlColToStr(ctx context.Context, col interface{}) string {
}
case time.Time:
return typedCol.Format("2006-01-02 15:04:05.999999 -0700 MST")
case sql.Point:
buf := make([]byte, 25)
WriteEWKBHeader(typedCol, buf)
WriteEWKBPointData(typedCol, buf[9:])
return SqlColToStr(ctx, buf)
case sql.Linestring:
buf := make([]byte, 9+4+16*len(typedCol.Points))
WriteEWKBHeader(typedCol, buf)
WriteEWKBLineData(typedCol, buf[9:])
return SqlColToStr(ctx, buf)
case sql.Polygon:
size := 0
for _, l := range typedCol.Lines {
size += 4 + 16*len(l.Points)
}
buf := make([]byte, 9+4+size)
WriteEWKBHeader(typedCol, buf)
WriteEWKBPolyData(typedCol, buf[9:])
return SqlColToStr(ctx, buf)
case sql.JSONValue:
s, err := typedCol.ToString(sql.NewContext(ctx))
if err != nil {
@@ -299,7 +370,7 @@ func SqlColToStr(ctx context.Context, col interface{}) string {
}
return s
default:
return fmt.Sprintf("%v", typedCol)
return fmt.Sprintf("no match: %v", typedCol)
}
}

View File

@@ -0,0 +1,202 @@
// Copyright 2021 Dolthub, Inc.
//
// Licensed 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 types
import (
"context"
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"github.com/dolthub/dolt/go/store/hash"
)
const (
LengthSize = 4
)
// Linestring is a Noms Value wrapper around a string.
type Linestring struct {
SRID uint32
Points []Point
}
// Value interface
func (v Linestring) Value(ctx context.Context) (Value, error) {
return v, nil
}
func (v Linestring) Equals(other Value) bool {
// Compare types
v2, ok := other.(Linestring)
if !ok {
return false
}
// Compare SRID
if v.SRID != v2.SRID {
return false
}
// Compare lengths of points
if len(v.Points) != len(v2.Points) {
return false
}
// Compare each point
for i := 0; i < len(v.Points); i++ {
if !v.Points[i].Equals(v2.Points[i]) {
return false
}
}
return true
}
func (v Linestring) Less(nbf *NomsBinFormat, other LesserValuable) (bool, error) {
// Compare types
v2, ok := other.(Linestring)
if !ok {
return LinestringKind < other.Kind(), nil
}
// TODO: should I even take this into account?
// Compare SRID
if v.SRID != v2.SRID {
return v.SRID < v2.SRID, nil
}
// Get shorter length
var n int
len1 := len(v.Points)
len2 := len(v2.Points)
if len1 < len2 {
n = len1
} else {
n = len2
}
// Compare each point until there's one that is less than
for i := 0; i < n; i++ {
if !v.Points[i].Equals(v2.Points[i]) {
return v.Points[i].Less(nbf, v2.Points[i])
}
}
// Determine based off length
return len1 < len2, nil
}
func (v Linestring) Hash(nbf *NomsBinFormat) (hash.Hash, error) {
return getHash(v, nbf)
}
func (v Linestring) isPrimitive() bool {
return true
}
func (v Linestring) WalkValues(ctx context.Context, cb ValueCallback) error {
for _, p := range v.Points {
if err := p.WalkValues(ctx, cb); err != nil {
return err
}
}
return nil
}
func (v Linestring) WalkRefs(nbf *NomsBinFormat, cb RefCallback) error {
return nil
}
func (v Linestring) typeOf() (*Type, error) {
return PrimitiveTypeMap[LinestringKind], nil
}
func (v Linestring) Kind() NomsKind {
return LinestringKind
}
func (v Linestring) valueReadWriter() ValueReadWriter {
return nil
}
// WriteEWKBLineData converts a Line into a byte array in EWKB format
func WriteEWKBLineData(l Linestring, buf []byte) {
// Write length of linestring
binary.LittleEndian.PutUint32(buf[:LengthSize], uint32(len(l.Points)))
// Append each point
for i, p := range l.Points {
WriteEWKBPointData(p, buf[LengthSize+PointDataSize*i:LengthSize+PointDataSize*(i+1)])
}
}
func (v Linestring) writeTo(w nomsWriter, nbf *NomsBinFormat) error {
err := LinestringKind.writeTo(w, nbf)
if err != nil {
return err
}
// Allocate buffer for linestring
buf := make([]byte, EWKBHeaderSize+LengthSize+PointDataSize*len(v.Points))
// Write header and data to buffer
WriteEWKBHeader(v, buf)
WriteEWKBLineData(v, buf[EWKBHeaderSize:])
w.writeString(string(buf))
return nil
}
// ParseEWKBLine converts the data portion of a WKB point to Linestring
// Very similar logic to the function in GMS
func ParseEWKBLine(buf []byte, srid uint32) Linestring {
// Read length of linestring
numPoints := binary.LittleEndian.Uint32(buf[:4])
// Parse points
points := make([]Point, numPoints)
for i := uint32(0); i < numPoints; i++ {
points[i] = ParseEWKBPoint(buf[LengthSize+PointDataSize*i:LengthSize+PointDataSize*(i+1)], srid)
}
return Linestring{SRID: srid, Points: points}
}
func readLinestring(nbf *NomsBinFormat, b *valueDecoder) (Linestring, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != LinestringID {
return Linestring{}, errors.New("not a linestring")
}
return ParseEWKBLine(buf[EWKBHeaderSize:], srid), nil
}
func (v Linestring) readFrom(nbf *NomsBinFormat, b *binaryNomsReader) (Value, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != LinestringID {
return nil, errors.New("not a linestring")
}
return ParseEWKBLine(buf[EWKBHeaderSize:], srid), nil
}
func (v Linestring) skip(nbf *NomsBinFormat, b *binaryNomsReader) {
b.skipString()
}
func (v Linestring) HumanReadableString() string {
points := make([]string, len(v.Points))
for i, p := range v.Points {
points[i] = p.HumanReadableString()
}
s := fmt.Sprintf("SRID: %d LINESTRING(%s)", v.SRID, strings.Join(points, ","))
return strconv.Quote(s)
}

View File

@@ -56,6 +56,9 @@ const (
TimestampKind
DecimalKind
JSONKind
PointKind
LinestringKind
PolygonKind
UnknownKind NomsKind = 255
)
@@ -83,6 +86,9 @@ func init() {
KindToType[TimestampKind] = Timestamp{}
KindToType[DecimalKind] = Decimal{}
KindToType[JSONKind] = JSON{}
KindToType[PointKind] = Point{}
KindToType[LinestringKind] = Linestring{}
KindToType[PolygonKind] = Polygon{}
SupportedKinds[BlobKind] = true
SupportedKinds[BoolKind] = true
@@ -106,6 +112,9 @@ func init() {
SupportedKinds[TimestampKind] = true
SupportedKinds[DecimalKind] = true
SupportedKinds[JSONKind] = true
SupportedKinds[PointKind] = true
SupportedKinds[LinestringKind] = true
SupportedKinds[PolygonKind] = true
}
var KindToTypeSlice []Value
@@ -134,6 +143,9 @@ var KindToString = map[NomsKind]string{
TimestampKind: "Timestamp",
DecimalKind: "Decimal",
JSONKind: "JSON",
PointKind: "Point",
LinestringKind: "Linestring",
PolygonKind: "Polygon",
}
// String returns the name of the kind.

181
go/store/types/point.go Normal file
View File

@@ -0,0 +1,181 @@
// Copyright 2021 Dolthub, Inc.
//
// Licensed 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 types
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"strconv"
"github.com/dolthub/dolt/go/store/hash"
)
const (
SRIDSize = 4
EndianSize = 1
TypeSize = 4
EWKBHeaderSize = SRIDSize + EndianSize + TypeSize
PointDataSize = 16
PointID = 1
LinestringID = 2
PolygonID = 3
)
// Point is a Noms Value wrapper around the primitive string type (for now).
type Point struct {
SRID uint32
X float64
Y float64
}
// Value interface
func (v Point) Value(ctx context.Context) (Value, error) {
return v, nil
}
func (v Point) Equals(other Value) bool {
if v2, ok := other.(Point); ok {
return v.SRID == v2.SRID && v.X == v2.X && v.Y == v2.Y
}
return false
}
func (v Point) Less(nbf *NomsBinFormat, other LesserValuable) (bool, error) {
if v2, ok := other.(Point); ok {
return v.SRID < v2.SRID || v.X < v2.X || v.Y < v2.Y, nil
}
return PointKind < other.Kind(), nil
}
func (v Point) Hash(nbf *NomsBinFormat) (hash.Hash, error) {
return getHash(v, nbf)
}
func (v Point) isPrimitive() bool {
return true
}
func (v Point) WalkValues(ctx context.Context, cb ValueCallback) error {
return cb(v)
}
func (v Point) WalkRefs(nbf *NomsBinFormat, cb RefCallback) error {
return nil
}
func (v Point) typeOf() (*Type, error) {
return PrimitiveTypeMap[PointKind], nil
}
func (v Point) Kind() NomsKind {
return PointKind
}
func (v Point) valueReadWriter() ValueReadWriter {
return nil
}
// WriteEWKBHeader writes the SRID, endianness, and type to the byte buffer
// This function assumes v is a valid spatial type
func WriteEWKBHeader(v interface{}, buf []byte) {
// Write endianness byte (always little endian)
buf[SRIDSize] = 1
// Parse data
switch v := v.(type) {
case Point:
// Write SRID and type
binary.LittleEndian.PutUint32(buf[0:SRIDSize], v.SRID)
binary.LittleEndian.PutUint32(buf[SRIDSize+EndianSize:EWKBHeaderSize], PointID)
case Linestring:
binary.LittleEndian.PutUint32(buf[0:SRIDSize], v.SRID)
binary.LittleEndian.PutUint32(buf[SRIDSize+EndianSize:EWKBHeaderSize], LinestringID)
case Polygon:
binary.LittleEndian.PutUint32(buf[0:SRIDSize], v.SRID)
binary.LittleEndian.PutUint32(buf[SRIDSize+EndianSize:EWKBHeaderSize], PolygonID)
}
}
// WriteEWKBPointData converts a Point into a byte array in EWKB format
// Very similar to function in GMS
func WriteEWKBPointData(p Point, buf []byte) {
binary.LittleEndian.PutUint64(buf[:PointDataSize/2], math.Float64bits(p.X))
binary.LittleEndian.PutUint64(buf[PointDataSize/2:], math.Float64bits(p.Y))
}
func (v Point) writeTo(w nomsWriter, nbf *NomsBinFormat) error {
// Mark as PointKind
err := PointKind.writeTo(w, nbf)
if err != nil {
return err
}
// Allocate buffer for point 4 + 1 + 4 + 16
buf := make([]byte, EWKBHeaderSize+PointDataSize)
// Write header and data to buffer
WriteEWKBHeader(v, buf)
WriteEWKBPointData(v, buf[EWKBHeaderSize:])
w.writeString(string(buf))
return nil
}
// ParseEWKBHeader converts the header potion of a EWKB byte array to srid, endianness, and geometry type
func ParseEWKBHeader(buf []byte) (uint32, bool, uint32) {
srid := binary.LittleEndian.Uint32(buf[0:SRIDSize]) // First 4 bytes is SRID always in little endian
isBig := buf[SRIDSize] == 0 // Next byte is endianness
geomType := binary.LittleEndian.Uint32(buf[SRIDSize+EndianSize : EWKBHeaderSize]) // Next 4 bytes is type
return srid, isBig, geomType
}
// ParseEWKBPoint converts the data portion of a WKB point to Point
// Very similar logic to the function in GMS
func ParseEWKBPoint(buf []byte, srid uint32) Point {
// Read floats x and y
x := math.Float64frombits(binary.LittleEndian.Uint64(buf[:PointDataSize/2]))
y := math.Float64frombits(binary.LittleEndian.Uint64(buf[PointDataSize/2:]))
return Point{SRID: srid, X: x, Y: y}
}
func readPoint(nbf *NomsBinFormat, b *valueDecoder) (Point, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf) // Assume it's always little endian
if geomType != PointID {
return Point{}, errors.New("not a point")
}
return ParseEWKBPoint(buf[EWKBHeaderSize:], srid), nil
}
func (v Point) readFrom(nbf *NomsBinFormat, b *binaryNomsReader) (Value, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf) // Assume it's always little endian
if geomType != PointID {
return Point{}, errors.New("not a point")
}
return ParseEWKBPoint(buf[EWKBHeaderSize:], srid), nil
}
func (v Point) skip(nbf *NomsBinFormat, b *binaryNomsReader) {
b.skipString()
}
func (v Point) HumanReadableString() string {
s := fmt.Sprintf("SRID: %d POINT(%s %s)", v.SRID, strconv.FormatFloat(v.X, 'g', -1, 64), strconv.FormatFloat(v.Y, 'g', -1, 64))
return strconv.Quote(s)
}

205
go/store/types/polygon.go Normal file
View File

@@ -0,0 +1,205 @@
// Copyright 2021 Dolthub, Inc.
//
// Licensed 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 types
import (
"context"
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"github.com/dolthub/dolt/go/store/hash"
)
// Polygon is a Noms Value wrapper around a string.
type Polygon struct {
SRID uint32
Lines []Linestring
}
// Value interface
func (v Polygon) Value(ctx context.Context) (Value, error) {
return v, nil
}
func (v Polygon) Equals(other Value) bool {
// Compare types
v2, ok := other.(Polygon)
if !ok {
return false
}
// Compare SRID
if v.SRID != v2.SRID {
return false
}
// Compare lengths of lines
if len(v.Lines) != len(v2.Lines) {
return false
}
// Compare each line
for i := 0; i < len(v.Lines); i++ {
if !v.Lines[i].Equals(v2.Lines[i]) {
return false
}
}
return true
}
func (v Polygon) Less(nbf *NomsBinFormat, other LesserValuable) (bool, error) {
// Compare types
v2, ok := other.(Polygon)
if !ok {
return PolygonKind < other.Kind(), nil
}
// Compare SRID
if v.SRID != v2.SRID {
return v.SRID < v2.SRID, nil
}
// Get shorter length
var n int
len1 := len(v.Lines)
len2 := len(v2.Lines)
if len1 < len2 {
n = len1
} else {
n = len2
}
// Compare each point until there is one that is less
for i := 0; i < n; i++ {
if !v.Lines[i].Equals(v2.Lines[i]) {
return v.Lines[i].Less(nbf, v2.Lines[i])
}
}
// Determine based off length
return len1 < len2, nil
}
func (v Polygon) Hash(nbf *NomsBinFormat) (hash.Hash, error) {
return getHash(v, nbf)
}
func (v Polygon) isPrimitive() bool {
return true
}
func (v Polygon) WalkValues(ctx context.Context, cb ValueCallback) error {
for _, l := range v.Lines {
if err := l.WalkValues(ctx, cb); err != nil {
return err
}
}
return nil
}
func (v Polygon) WalkRefs(nbf *NomsBinFormat, cb RefCallback) error {
return nil
}
func (v Polygon) typeOf() (*Type, error) {
return PrimitiveTypeMap[PolygonKind], nil
}
func (v Polygon) Kind() NomsKind {
return PolygonKind
}
func (v Polygon) valueReadWriter() ValueReadWriter {
return nil
}
// WriteEWKBPolyData converts a Polygon into a byte array in EWKB format
func WriteEWKBPolyData(p Polygon, buf []byte) {
// Write length of polygon
binary.LittleEndian.PutUint32(buf[:LengthSize], uint32(len(p.Lines)))
// Write each line
start, stop := 0, LengthSize
for _, l := range p.Lines {
start, stop = stop, stop+LengthSize+PointDataSize*len(l.Points)
WriteEWKBLineData(l, buf[start:stop])
}
}
func (v Polygon) writeTo(w nomsWriter, nbf *NomsBinFormat) error {
err := PolygonKind.writeTo(w, nbf)
if err != nil {
return err
}
// Calculate space for polygon buffer
size := 0
for _, l := range v.Lines {
size += LengthSize + PointDataSize*len(l.Points)
}
// Allocate buffer for poly
buf := make([]byte, EWKBHeaderSize+LengthSize+size)
// Write header and data to buffer
WriteEWKBHeader(v, buf)
WriteEWKBPolyData(v, buf[EWKBHeaderSize:])
w.writeString(string(buf))
return nil
}
// ParseEWKBPoly converts the data portions of a WKB polygon to Polygon
// Very similar logic to the function in GMS
func ParseEWKBPoly(buf []byte, srid uint32) Polygon {
// Read length of Polygon
numLines := binary.LittleEndian.Uint32(buf[:LengthSize])
// Parse lines
s := LengthSize
lines := make([]Linestring, numLines)
for i := uint32(0); i < numLines; i++ {
lines[i] = ParseEWKBLine(buf[s:], srid)
s += LengthSize * PointDataSize * len(lines[i].Points)
}
return Polygon{SRID: srid, Lines: lines}
}
func readPolygon(nbf *NomsBinFormat, b *valueDecoder) (Polygon, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != PolygonID {
return Polygon{}, errors.New("not a polygon")
}
return ParseEWKBPoly(buf[EWKBHeaderSize:], srid), nil
}
func (v Polygon) readFrom(nbf *NomsBinFormat, b *binaryNomsReader) (Value, error) {
buf := []byte(b.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != PolygonID {
return nil, errors.New("not a polygon")
}
return ParseEWKBPoly(buf[EWKBHeaderSize:], srid), nil
}
func (v Polygon) skip(nbf *NomsBinFormat, b *binaryNomsReader) {
b.skipString()
}
func (v Polygon) HumanReadableString() string {
lines := make([]string, len(v.Lines))
for i, l := range v.Lines {
lines[i] = l.HumanReadableString()
}
s := fmt.Sprintf("SRID: %d POLYGON(%s)", v.SRID, strings.Join(lines, ","))
return strconv.Quote(s)
}

View File

@@ -46,6 +46,9 @@ type CodecReader interface {
ReadInlineBlob() []byte
ReadTimestamp() (time.Time, error)
ReadDecimal() (decimal.Decimal, error)
ReadPoint() (Point, error)
ReadLinestring() (Linestring, error)
ReadPolygon() (Polygon, error)
ReadBlob() (Blob, error)
ReadJSON() (JSON, error)
}
@@ -80,6 +83,18 @@ func (r *valueDecoder) ReadBlob() (Blob, error) {
return newBlob(seq), nil
}
func (r *valueDecoder) ReadPoint() (Point, error) {
return readPoint(nil, r)
}
func (r *valueDecoder) ReadLinestring() (Linestring, error) {
return readLinestring(nil, r)
}
func (r *valueDecoder) ReadPolygon() (Polygon, error) {
return readPolygon(nil, r)
}
func (r *valueDecoder) ReadJSON() (JSON, error) {
return readJSON(r.vrw.Format(), r)
}
@@ -356,6 +371,30 @@ func (r *valueDecoder) readValue(nbf *NomsBinFormat) (Value, error) {
return r.readTuple(nbf)
case JSONKind:
return r.ReadJSON()
case PointKind:
r.skipKind()
buf := []byte(r.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != PointID {
return nil, ErrUnknownType
}
return ParseEWKBPoint(buf[EWKBHeaderSize:], srid), nil
case LinestringKind:
r.skipKind()
buf := []byte(r.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != LinestringID {
return nil, ErrUnknownType
}
return ParseEWKBLine(buf[EWKBHeaderSize:], srid), nil
case PolygonKind:
r.skipKind()
buf := []byte(r.ReadString())
srid, _, geomType := ParseEWKBHeader(buf)
if geomType != PolygonID {
return nil, ErrUnknownType
}
return ParseEWKBPoly(buf[EWKBHeaderSize:], srid), nil
case TypeKind:
r.skipKind()
return r.readType()
@@ -405,6 +444,15 @@ func (r *valueDecoder) SkipValue(nbf *NomsBinFormat) error {
case StringKind:
r.skipKind()
r.skipString()
case PointKind:
r.skipKind()
r.skipString()
case LinestringKind:
r.skipKind()
r.skipString()
case PolygonKind:
r.skipKind()
r.skipString()
case ListKind:
err := r.skipList(nbf)
if err != nil {

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bats
load $BATS_TEST_DIRNAME/helper/common.bash
setup() {
setup_common
}
teardown() {
assert_feature_version
teardown_common
}
@test "sql-spatial-types: can't make spatial types without enabling feature flag" {
run dolt sql -q "create table point_tbl (p point)"
[ "$status" -eq 1 ]
[[ "$output" =~ "cannot be made" ]] || false
}
@test "sql-spatial-types: can make spatial types with flag" {
DOLT_ENABLE_SPATIAL_TYPES=true run dolt sql -q "create table point_tbl (p point)"
[ "$status" -eq 0 ]
[[ "$output" = "" ]] || false
}