mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(xugu): expose spatial query values as WKT
This commit is contained in:
+61
-28
@@ -238,6 +238,12 @@ var xuguDataTypes = []string{
|
||||
"DOUBLE[]",
|
||||
"FLOAT",
|
||||
"GUID",
|
||||
"GEOMETRY",
|
||||
"GEOGRAPHY",
|
||||
"BOX2D",
|
||||
"BOX3D",
|
||||
"SPHEROID",
|
||||
"RASTER",
|
||||
"INT",
|
||||
"INTEGER",
|
||||
"INTEGER[]",
|
||||
@@ -311,12 +317,14 @@ type queryOptions struct {
|
||||
}
|
||||
|
||||
type queryResult struct {
|
||||
Columns []string `json:"columns"`
|
||||
ColumnTypes []string `json:"column_types"`
|
||||
Rows [][]any `json:"rows"`
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
ExecutionTimeMS int64 `json:"execution_time_ms"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Columns []string `json:"columns"`
|
||||
ColumnTypes []string `json:"column_types"`
|
||||
SpatialColumns []spatialColumn `json:"spatial_columns,omitempty"`
|
||||
SpatialValues [][]*uint32 `json:"spatial_values,omitempty"`
|
||||
Rows [][]any `json:"rows"`
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
ExecutionTimeMS int64 `json:"execution_time_ms"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
func (r queryResult) MarshalJSON() ([]byte, error) {
|
||||
@@ -335,14 +343,16 @@ func (r queryResult) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
type queryPageResult struct {
|
||||
Columns []string `json:"columns"`
|
||||
ColumnTypes []string `json:"column_types"`
|
||||
Rows [][]any `json:"rows"`
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
ExecutionTimeMS int64 `json:"execution_time_ms"`
|
||||
Truncated bool `json:"truncated"`
|
||||
SessionID *string `json:"session_id"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Columns []string `json:"columns"`
|
||||
ColumnTypes []string `json:"column_types"`
|
||||
SpatialColumns []spatialColumn `json:"spatial_columns,omitempty"`
|
||||
SpatialValues [][]*uint32 `json:"spatial_values,omitempty"`
|
||||
Rows [][]any `json:"rows"`
|
||||
AffectedRows int64 `json:"affected_rows"`
|
||||
ExecutionTimeMS int64 `json:"execution_time_ms"`
|
||||
Truncated bool `json:"truncated"`
|
||||
SessionID *string `json:"session_id"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
func (r queryPageResult) MarshalJSON() ([]byte, error) {
|
||||
@@ -361,11 +371,13 @@ func (r queryPageResult) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
type querySession struct {
|
||||
rows *sql.Rows
|
||||
columns []string
|
||||
columnTypes []string
|
||||
pending []any
|
||||
remaining int
|
||||
rows *sql.Rows
|
||||
columns []string
|
||||
columnTypes []string
|
||||
scanner *xuguRowScanner
|
||||
pending []any
|
||||
pendingSpatial []*uint32
|
||||
remaining int
|
||||
}
|
||||
|
||||
type databaseInfo struct {
|
||||
@@ -3535,6 +3547,8 @@ func (s *server) executeQueryPage(opts queryOptions, pageSize int) (queryPageRes
|
||||
return queryPageResult{
|
||||
Columns: result.Columns,
|
||||
ColumnTypes: result.ColumnTypes,
|
||||
SpatialColumns: result.SpatialColumns,
|
||||
SpatialValues: result.SpatialValues,
|
||||
Rows: result.Rows,
|
||||
AffectedRows: result.AffectedRows,
|
||||
ExecutionTimeMS: result.ExecutionTimeMS,
|
||||
@@ -3553,11 +3567,19 @@ func (s *server) executeQueryPage(opts queryOptions, pageSize int) (queryPageRes
|
||||
return queryPageResult{}, err
|
||||
}
|
||||
columnTypes := columnTypeNames(rows)
|
||||
rowScanner := newXuguRowScanner(len(columns), columnTypes)
|
||||
columnTypes = rowScanner.spatial.columnTypes
|
||||
maxRows := opts.MaxRows
|
||||
if maxRows <= 0 {
|
||||
maxRows = defaultMaxRows
|
||||
}
|
||||
session := &querySession{rows: rows, columns: columns, columnTypes: columnTypes, remaining: maxRows}
|
||||
session := &querySession{
|
||||
rows: rows,
|
||||
columns: columns,
|
||||
columnTypes: columnTypes,
|
||||
scanner: rowScanner,
|
||||
remaining: maxRows,
|
||||
}
|
||||
result, err := readQuerySessionPage(session, pageSize)
|
||||
result.ExecutionTimeMS = time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
@@ -3619,37 +3641,42 @@ func readQuerySessionPage(session *querySession, pageSize int) (queryPageResult,
|
||||
pageSize = defaultMaxRows
|
||||
}
|
||||
result := queryPageResult{Columns: session.columns, ColumnTypes: session.columnTypes, Rows: [][]any{}, SessionID: nil, HasMore: false}
|
||||
spatialValues := make([][]*uint32, 0, pageSize)
|
||||
for len(result.Rows) < pageSize && session.remaining > 0 {
|
||||
if session.pending != nil {
|
||||
result.Rows = append(result.Rows, session.pending)
|
||||
spatialValues = append(spatialValues, session.pendingSpatial)
|
||||
session.pending = nil
|
||||
session.pendingSpatial = nil
|
||||
session.remaining--
|
||||
continue
|
||||
}
|
||||
if !session.rows.Next() {
|
||||
return result, session.rows.Err()
|
||||
return finishXuguSpatialPage(result, session.scanner, spatialValues), session.rows.Err()
|
||||
}
|
||||
row, err := scanRow(session.rows, len(session.columns))
|
||||
row, rowSpatial, err := session.scanner.scan(session.rows)
|
||||
if err != nil {
|
||||
return queryPageResult{}, err
|
||||
}
|
||||
result.Rows = append(result.Rows, row)
|
||||
spatialValues = append(spatialValues, rowSpatial)
|
||||
session.remaining--
|
||||
}
|
||||
if session.remaining <= 0 {
|
||||
result.Truncated = true
|
||||
return result, nil
|
||||
return finishXuguSpatialPage(result, session.scanner, spatialValues), nil
|
||||
}
|
||||
if session.rows.Next() {
|
||||
row, err := scanRow(session.rows, len(session.columns))
|
||||
row, rowSpatial, err := session.scanner.scan(session.rows)
|
||||
if err != nil {
|
||||
return queryPageResult{}, err
|
||||
}
|
||||
session.pending = row
|
||||
session.pendingSpatial = rowSpatial
|
||||
result.HasMore = true
|
||||
return result, nil
|
||||
return finishXuguSpatialPage(result, session.scanner, spatialValues), nil
|
||||
}
|
||||
return result, session.rows.Err()
|
||||
return finishXuguSpatialPage(result, session.scanner, spatialValues), session.rows.Err()
|
||||
}
|
||||
|
||||
func (s *server) executeQuery(opts queryOptions) (queryResult, error) {
|
||||
@@ -3696,18 +3723,24 @@ func (s *server) executeSelect(sqlText string, maxRows int, timeoutSecs int) (qu
|
||||
if err != nil {
|
||||
return queryResult{}, err
|
||||
}
|
||||
result := queryResult{Columns: columns, ColumnTypes: columnTypeNames(rows), Rows: [][]any{}}
|
||||
columnTypes := columnTypeNames(rows)
|
||||
scanner := newXuguRowScanner(len(columns), columnTypes)
|
||||
columnTypes = scanner.spatial.columnTypes
|
||||
result := queryResult{Columns: columns, ColumnTypes: columnTypes, Rows: [][]any{}}
|
||||
spatialValues := make([][]*uint32, 0)
|
||||
for rows.Next() {
|
||||
if len(result.Rows) >= maxRows {
|
||||
result.Truncated = true
|
||||
break
|
||||
}
|
||||
values, err := scanRow(rows, len(columns))
|
||||
values, rowSpatial, err := scanner.scan(rows)
|
||||
if err != nil {
|
||||
return queryResult{}, err
|
||||
}
|
||||
result.Rows = append(result.Rows, values)
|
||||
spatialValues = append(spatialValues, rowSpatial)
|
||||
}
|
||||
result.SpatialColumns, result.SpatialValues = xuguSpatialResultMetadata(scanner, spatialValues)
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// spatialColumn and the corresponding per-cell SRID values are part of the
|
||||
// common agent query-result protocol consumed by the desktop map preview.
|
||||
type spatialColumn struct {
|
||||
ColumnIndex int `json:"column_index"`
|
||||
SRID *uint32 `json:"srid"`
|
||||
}
|
||||
|
||||
type xuguRowScanner struct {
|
||||
values []any
|
||||
targets []any
|
||||
spatial *xuguSpatialDecoder
|
||||
}
|
||||
|
||||
func newXuguRowScanner(count int, columnTypes []string) *xuguRowScanner {
|
||||
if len(columnTypes) < count {
|
||||
expanded := make([]string, count)
|
||||
copy(expanded, columnTypes)
|
||||
columnTypes = expanded
|
||||
}
|
||||
scanner := &xuguRowScanner{
|
||||
values: make([]any, count),
|
||||
targets: make([]any, count),
|
||||
spatial: newXuguSpatialDecoder(columnTypes),
|
||||
}
|
||||
for index := range scanner.targets {
|
||||
scanner.targets[index] = &scanner.values[index]
|
||||
}
|
||||
return scanner
|
||||
}
|
||||
|
||||
func (scanner *xuguRowScanner) scan(rows *sql.Rows) ([]any, []*uint32, error) {
|
||||
for index := range scanner.values {
|
||||
scanner.values[index] = nil
|
||||
}
|
||||
if err := rows.Scan(scanner.targets...); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
result := make([]any, len(scanner.values))
|
||||
copy(result, scanner.values)
|
||||
return scanner.spatial.normalizeRow(result)
|
||||
}
|
||||
|
||||
type xuguSpatialDecoder struct {
|
||||
columnTypes []string
|
||||
indices []int
|
||||
candidates []int
|
||||
spatial []bool
|
||||
observed bool
|
||||
sridByColumn map[int]uint32
|
||||
}
|
||||
|
||||
func newXuguSpatialDecoder(columnTypes []string) *xuguSpatialDecoder {
|
||||
decoder := &xuguSpatialDecoder{
|
||||
columnTypes: columnTypes,
|
||||
spatial: make([]bool, len(columnTypes)),
|
||||
sridByColumn: map[int]uint32{},
|
||||
}
|
||||
for index, columnType := range columnTypes {
|
||||
if isXuguSpatialColumnType(columnType) {
|
||||
decoder.indices = append(decoder.indices, index)
|
||||
decoder.spatial[index] = true
|
||||
continue
|
||||
}
|
||||
// The current Xugu Go driver leaves DatabaseTypeName empty for direct
|
||||
// GEOMETRY/GEOGRAPHY projections. Keep only those unknown columns as
|
||||
// candidates; explicit BLOB/BINARY columns are never auto-classified.
|
||||
if strings.TrimSpace(columnType) == "" {
|
||||
decoder.candidates = append(decoder.candidates, index)
|
||||
}
|
||||
}
|
||||
return decoder
|
||||
}
|
||||
|
||||
func isXuguSpatialColumnType(columnType string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(columnType))
|
||||
if index := strings.LastIndexByte(normalized, '.'); index >= 0 {
|
||||
normalized = normalized[index+1:]
|
||||
}
|
||||
if index := strings.IndexByte(normalized, '('); index >= 0 {
|
||||
normalized = normalized[:index]
|
||||
}
|
||||
return normalized == "geometry" || normalized == "geography"
|
||||
}
|
||||
|
||||
func (decoder *xuguSpatialDecoder) normalizeRow(values []any) ([]any, []*uint32, error) {
|
||||
rowSRIDs := make([]*uint32, len(values))
|
||||
for _, index := range decoder.candidates {
|
||||
if index >= len(values) || decoder.spatial[index] {
|
||||
continue
|
||||
}
|
||||
decoded, srid, recognized := decodeXuguSpatialValue(values[index])
|
||||
if !recognized {
|
||||
continue
|
||||
}
|
||||
decoder.indices = append(decoder.indices, index)
|
||||
decoder.spatial[index] = true
|
||||
decoder.columnTypes[index] = "GEOMETRY"
|
||||
values[index] = decoded
|
||||
rowSRIDs[index] = srid
|
||||
decoder.recordSRID(index, srid)
|
||||
}
|
||||
for _, index := range decoder.indices {
|
||||
if index >= len(values) || rowSRIDs[index] != nil {
|
||||
continue
|
||||
}
|
||||
decoded, srid, _ := decodeXuguSpatialValue(values[index])
|
||||
values[index] = decoded
|
||||
rowSRIDs[index] = srid
|
||||
decoder.recordSRID(index, srid)
|
||||
}
|
||||
for index, value := range values {
|
||||
if !decoder.spatial[index] {
|
||||
values[index] = normalizeValue(value)
|
||||
}
|
||||
}
|
||||
if len(decoder.indices) > 0 {
|
||||
decoder.observed = true
|
||||
}
|
||||
return values, rowSRIDs, nil
|
||||
}
|
||||
|
||||
func (decoder *xuguSpatialDecoder) recordSRID(index int, srid *uint32) {
|
||||
if srid == nil {
|
||||
return
|
||||
}
|
||||
if _, exists := decoder.sridByColumn[index]; !exists {
|
||||
decoder.sridByColumn[index] = *srid
|
||||
}
|
||||
}
|
||||
|
||||
func (decoder *xuguSpatialDecoder) columns() []spatialColumn {
|
||||
if !decoder.observed {
|
||||
return nil
|
||||
}
|
||||
columns := make([]spatialColumn, 0, len(decoder.indices))
|
||||
for _, index := range decoder.indices {
|
||||
var srid *uint32
|
||||
if value, found := decoder.sridByColumn[index]; found {
|
||||
srid = uint32Pointer(value)
|
||||
}
|
||||
columns = append(columns, spatialColumn{ColumnIndex: index, SRID: srid})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
func xuguSpatialResultMetadata(scanner *xuguRowScanner, values [][]*uint32) ([]spatialColumn, [][]*uint32) {
|
||||
columns := scanner.spatial.columns()
|
||||
if len(columns) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return columns, values
|
||||
}
|
||||
|
||||
func finishXuguSpatialPage(result queryPageResult, scanner *xuguRowScanner, values [][]*uint32) queryPageResult {
|
||||
result.SpatialColumns, result.SpatialValues = xuguSpatialResultMetadata(scanner, values)
|
||||
return result
|
||||
}
|
||||
|
||||
func decodeXuguSpatialValue(value any) (any, *uint32, bool) {
|
||||
if value == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case []byte:
|
||||
if decoded, ok := decodeXuguWKB(typed); ok {
|
||||
return decoded.WKT, decoded.SRID, true
|
||||
}
|
||||
if utf8.Valid(typed) {
|
||||
return decodeXuguSpatialText(string(typed))
|
||||
}
|
||||
return "0x" + hex.EncodeToString(typed), nil, false
|
||||
case string:
|
||||
return decodeXuguSpatialText(typed)
|
||||
default:
|
||||
return fmt.Sprint(normalizeValue(value)), nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func decodeXuguSpatialText(value string) (any, *uint32, bool) {
|
||||
if strings.HasPrefix(strings.ToUpper(value), "SRID=") {
|
||||
if separator := strings.IndexByte(value, ';'); separator > 5 {
|
||||
if parsed, err := strconv.ParseUint(value[5:separator], 10, 32); err == nil {
|
||||
srid := uint32(parsed)
|
||||
wkt := strings.TrimSpace(value[separator+1:])
|
||||
if isXuguWKT(wkt) {
|
||||
return wkt, uint32Pointer(srid), true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw, ok := parseXuguSpatialHex(value); ok {
|
||||
if decoded, decodedOK := decodeXuguWKB(raw); decodedOK {
|
||||
return decoded.WKT, decoded.SRID, true
|
||||
}
|
||||
}
|
||||
// Plain WKT is valid for a declared spatial column. It is intentionally
|
||||
// not used to promote an unknown column, because free-form text can look
|
||||
// like WKT and must not change a non-spatial result's representation.
|
||||
return value, nil, false
|
||||
}
|
||||
|
||||
func isXuguWKT(value string) bool {
|
||||
upper := strings.ToUpper(strings.TrimSpace(value))
|
||||
for _, prefix := range []string{"POINT", "LINESTRING", "POLYGON", "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"} {
|
||||
if strings.HasPrefix(upper, prefix+"(") || strings.HasPrefix(upper, prefix+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseXuguSpatialHex(value string) ([]byte, bool) {
|
||||
normalized := strings.TrimSpace(value)
|
||||
if strings.HasPrefix(normalized, "0x") || strings.HasPrefix(normalized, "0X") || strings.HasPrefix(normalized, `\x`) || strings.HasPrefix(normalized, `\X`) {
|
||||
normalized = normalized[2:]
|
||||
}
|
||||
if len(normalized) < 10 || len(normalized)%2 != 0 || (normalized[:2] != "00" && normalized[:2] != "01") {
|
||||
return nil, false
|
||||
}
|
||||
raw, err := hex.DecodeString(normalized)
|
||||
return raw, err == nil
|
||||
}
|
||||
|
||||
func uint32Pointer(value uint32) *uint32 {
|
||||
result := value
|
||||
return &result
|
||||
}
|
||||
|
||||
type decodedXuguWKB struct {
|
||||
WKT string
|
||||
SRID *uint32
|
||||
}
|
||||
|
||||
type xuguWKBDimensions struct{ z, m bool }
|
||||
|
||||
func (dimensions xuguWKBDimensions) size() int {
|
||||
size := 2
|
||||
if dimensions.z {
|
||||
size++
|
||||
}
|
||||
if dimensions.m {
|
||||
size++
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (dimensions xuguWKBDimensions) suffix() string {
|
||||
if dimensions.z && dimensions.m {
|
||||
return " ZM"
|
||||
}
|
||||
if dimensions.z {
|
||||
return " Z"
|
||||
}
|
||||
if dimensions.m {
|
||||
return " M"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type xuguWKBReader struct {
|
||||
raw []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) uint32(order binary.ByteOrder) (uint32, bool) {
|
||||
if reader.pos+4 > len(reader.raw) {
|
||||
return 0, false
|
||||
}
|
||||
value := order.Uint32(reader.raw[reader.pos : reader.pos+4])
|
||||
reader.pos += 4
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) float64(order binary.ByteOrder) (float64, bool) {
|
||||
if reader.pos+8 > len(reader.raw) {
|
||||
return 0, false
|
||||
}
|
||||
value := math.Float64frombits(order.Uint64(reader.raw[reader.pos : reader.pos+8]))
|
||||
reader.pos += 8
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) byteOrder() (binary.ByteOrder, bool) {
|
||||
if reader.pos >= len(reader.raw) {
|
||||
return nil, false
|
||||
}
|
||||
value := reader.raw[reader.pos]
|
||||
reader.pos++
|
||||
if value == 0 {
|
||||
return binary.BigEndian, true
|
||||
}
|
||||
if value == 1 {
|
||||
return binary.LittleEndian, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func decodeXuguWKB(raw []byte) (decodedXuguWKB, bool) {
|
||||
reader := &xuguWKBReader{raw: raw}
|
||||
wkt, srid, ok := reader.geometry(0)
|
||||
if !ok || reader.pos != len(raw) {
|
||||
return decodedXuguWKB{}, false
|
||||
}
|
||||
return decodedXuguWKB{WKT: wkt, SRID: srid}, true
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) geometry(depth int) (string, *uint32, bool) {
|
||||
if depth > 64 {
|
||||
return "", nil, false
|
||||
}
|
||||
order, ok := reader.byteOrder()
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
typeWord, ok := reader.uint32(order)
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
kind, dimensions, hasSRID := parseXuguWKBType(typeWord)
|
||||
var srid *uint32
|
||||
if hasSRID {
|
||||
value, valueOK := reader.uint32(order)
|
||||
if !valueOK {
|
||||
return "", nil, false
|
||||
}
|
||||
if value != 0 {
|
||||
srid = uint32Pointer(value)
|
||||
}
|
||||
}
|
||||
name := map[uint32]string{1: "POINT", 2: "LINESTRING", 3: "POLYGON", 4: "MULTIPOINT", 5: "MULTILINESTRING", 6: "MULTIPOLYGON", 7: "GEOMETRYCOLLECTION"}[kind]
|
||||
if name == "" {
|
||||
return "", nil, false
|
||||
}
|
||||
suffix := dimensions.suffix()
|
||||
switch kind {
|
||||
case 1:
|
||||
point, empty, pointOK := reader.point(order, dimensions)
|
||||
if !pointOK {
|
||||
return "", nil, false
|
||||
}
|
||||
if empty {
|
||||
return name + suffix + " EMPTY", srid, true
|
||||
}
|
||||
return name + suffix + "(" + point + ")", srid, true
|
||||
case 2:
|
||||
points, pointsOK := reader.points(order, dimensions)
|
||||
if !pointsOK {
|
||||
return "", nil, false
|
||||
}
|
||||
if len(points) == 0 {
|
||||
return name + suffix + " EMPTY", srid, true
|
||||
}
|
||||
return name + suffix + "(" + strings.Join(points, ",") + ")", srid, true
|
||||
case 3:
|
||||
rings, ringsOK := reader.rings(order, dimensions)
|
||||
if !ringsOK {
|
||||
return "", nil, false
|
||||
}
|
||||
if len(rings) == 0 {
|
||||
return name + suffix + " EMPTY", srid, true
|
||||
}
|
||||
return name + suffix + "(" + strings.Join(rings, ",") + ")", srid, true
|
||||
case 4, 5, 6, 7:
|
||||
count, countOK := reader.uint32(order)
|
||||
if !countOK || uint64(count)*5 > uint64(len(reader.raw)-reader.pos) {
|
||||
return "", nil, false
|
||||
}
|
||||
parts := make([]string, int(count))
|
||||
for index := range parts {
|
||||
child, _, childOK := reader.geometry(depth + 1)
|
||||
if !childOK {
|
||||
return "", nil, false
|
||||
}
|
||||
switch kind {
|
||||
case 4:
|
||||
if !strings.HasPrefix(child, "POINT"+suffix) {
|
||||
return "", nil, false
|
||||
}
|
||||
rest := strings.TrimPrefix(child, "POINT"+suffix)
|
||||
if strings.HasPrefix(rest, " EMPTY") {
|
||||
parts[index] = "EMPTY"
|
||||
} else if strings.HasPrefix(rest, "(") && strings.HasSuffix(rest, ")") {
|
||||
parts[index] = "(" + strings.TrimSuffix(strings.TrimPrefix(rest, "("), ")") + ")"
|
||||
}
|
||||
case 5:
|
||||
parts[index] = trimXuguWKBChild(child, "LINESTRING", suffix)
|
||||
case 6:
|
||||
parts[index] = trimXuguWKBChild(child, "POLYGON", suffix)
|
||||
default:
|
||||
parts[index] = child
|
||||
}
|
||||
if parts[index] == "" {
|
||||
return "", nil, false
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return name + suffix + " EMPTY", srid, true
|
||||
}
|
||||
return name + suffix + "(" + strings.Join(parts, ",") + ")", srid, true
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
func trimXuguWKBChild(value, name, suffix string) string {
|
||||
prefix := name + suffix
|
||||
if !strings.HasPrefix(value, prefix+"(") || !strings.HasSuffix(value, ")") {
|
||||
return ""
|
||||
}
|
||||
return "(" + strings.TrimSuffix(strings.TrimPrefix(value, prefix+"("), ")") + ")"
|
||||
}
|
||||
|
||||
func parseXuguWKBType(typeWord uint32) (uint32, xuguWKBDimensions, bool) {
|
||||
kind := typeWord & 0x1fffffff
|
||||
dimensions := xuguWKBDimensions{z: typeWord&0x80000000 != 0, m: typeWord&0x40000000 != 0}
|
||||
hasSRID := typeWord&0x20000000 != 0
|
||||
switch {
|
||||
case kind >= 3000:
|
||||
dimensions.z, dimensions.m, kind = true, true, kind-3000
|
||||
case kind >= 2000:
|
||||
dimensions.m, kind = true, kind-2000
|
||||
case kind >= 1000:
|
||||
dimensions.z, kind = true, kind-1000
|
||||
}
|
||||
return kind, dimensions, hasSRID
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) point(order binary.ByteOrder, dimensions xuguWKBDimensions) (string, bool, bool) {
|
||||
values := make([]string, dimensions.size())
|
||||
empty := true
|
||||
for index := range values {
|
||||
value, ok := reader.float64(order)
|
||||
if !ok {
|
||||
return "", false, false
|
||||
}
|
||||
empty = empty && math.IsNaN(value)
|
||||
values[index] = strconv.FormatFloat(value, 'g', -1, 64)
|
||||
}
|
||||
return strings.Join(values, " "), empty, true
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) points(order binary.ByteOrder, dimensions xuguWKBDimensions) ([]string, bool) {
|
||||
count, ok := reader.uint32(order)
|
||||
if !ok || uint64(count)*uint64(dimensions.size())*8 > uint64(len(reader.raw)-reader.pos) {
|
||||
return nil, false
|
||||
}
|
||||
points := make([]string, int(count))
|
||||
for index := range points {
|
||||
point, empty, pointOK := reader.point(order, dimensions)
|
||||
if !pointOK || empty {
|
||||
return nil, false
|
||||
}
|
||||
points[index] = point
|
||||
}
|
||||
return points, true
|
||||
}
|
||||
|
||||
func (reader *xuguWKBReader) rings(order binary.ByteOrder, dimensions xuguWKBDimensions) ([]string, bool) {
|
||||
count, ok := reader.uint32(order)
|
||||
if !ok || uint64(count)*4 > uint64(len(reader.raw)-reader.pos) {
|
||||
return nil, false
|
||||
}
|
||||
rings := make([]string, int(count))
|
||||
for index := range rings {
|
||||
points, pointsOK := reader.points(order, dimensions)
|
||||
if !pointsOK {
|
||||
return nil, false
|
||||
}
|
||||
rings[index] = "(" + strings.Join(points, ",") + ")"
|
||||
}
|
||||
return rings, true
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeXuguSpatialValues(t *testing.T) {
|
||||
point := mustDecodeXuguHex(t, "0101000020E6100000C520B07268195D404E62105839F44340")
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
wantValue any
|
||||
wantSRID *uint32
|
||||
recognized bool
|
||||
}{
|
||||
{"raw EWKB", point, "POINT(116.397 39.908)", uint32Pointer(4326), true},
|
||||
{"hex EWKB", "0x0101000020E6100000C520B07268195D404E62105839F44340", "POINT(116.397 39.908)", uint32Pointer(4326), true},
|
||||
{"EWKT", "SRID=3857;POINT(1 2)", "POINT(1 2)", uint32Pointer(3857), true},
|
||||
{"invalid EWKT is not promoted", "SRID=3857;not a geometry", "SRID=3857;not a geometry", nil, false},
|
||||
{"WKT remains text for unknown columns", "POINT(1 2)", "POINT(1 2)", nil, false},
|
||||
{"NULL", nil, nil, nil, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
value, srid, recognized := decodeXuguSpatialValue(test.value)
|
||||
if !reflect.DeepEqual(value, test.wantValue) || !reflect.DeepEqual(srid, test.wantSRID) || recognized != test.recognized {
|
||||
t.Fatalf("decodeXuguSpatialValue(%v) = (%v, %v, %v), want (%v, %v, %v)", test.value, value, srid, recognized, test.wantValue, test.wantSRID, test.recognized)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguSpatialDecoderInfersBlankGeometryTypeWithoutPromotingText(t *testing.T) {
|
||||
columnTypes := []string{"INTEGER", "", "VARCHAR"}
|
||||
decoder := newXuguSpatialDecoder(columnTypes)
|
||||
point := mustDecodeXuguHex(t, "0101000020E6100000000000000000F03F0000000000000040")
|
||||
row, values, err := decoder.normalizeRow([]any{int64(1), point, "POINT(9 9)"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if row[1] != "POINT(1 2)" || row[2] != "POINT(9 9)" {
|
||||
t.Fatalf("unexpected normalized row: %v", row)
|
||||
}
|
||||
if columnTypes[1] != "GEOMETRY" || columnTypes[2] != "VARCHAR" {
|
||||
t.Fatalf("unexpected inferred column types: %v", columnTypes)
|
||||
}
|
||||
if len(values) != 3 || values[1] == nil || *values[1] != 4326 || values[2] != nil {
|
||||
t.Fatalf("unexpected spatial cell metadata: %v", values)
|
||||
}
|
||||
columns := decoder.columns()
|
||||
if len(columns) != 1 || columns[0].ColumnIndex != 1 || columns[0].SRID == nil || *columns[0].SRID != 4326 {
|
||||
t.Fatalf("unexpected spatial columns: %+v", columns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguSpatialDecoderKeepsOrdinaryBinaryColumnUntouched(t *testing.T) {
|
||||
columnTypes := []string{"BLOB", "BINARY"}
|
||||
decoder := newXuguSpatialDecoder(columnTypes)
|
||||
value := []byte{0x01, 0x02, 0x03}
|
||||
row, values, err := decoder.normalizeRow([]any{value, value})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoder.indices) != 0 || len(decoder.columns()) != 0 || len(values) != 2 {
|
||||
t.Fatalf("ordinary binary values were classified as spatial: columns=%+v values=%v", decoder.columns(), values)
|
||||
}
|
||||
if row[0] != string(value) || row[1] != string(value) {
|
||||
t.Fatalf("ordinary binary values changed: %v", row)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeXuguSpatialComplexEWKB(t *testing.T) {
|
||||
encoded := "0107000020E61000000200000001010000000000000000005D4000000000000044400102000000020000000000000000405D4000000000008044400000000000805D400000000000004540"
|
||||
decoded, ok := decodeXuguWKB(mustDecodeXuguHex(t, encoded))
|
||||
if !ok || decoded.WKT != "GEOMETRYCOLLECTION(POINT(116 40),LINESTRING(117 41,118 42))" || decoded.SRID == nil || *decoded.SRID != 4326 {
|
||||
t.Fatalf("unexpected complex EWKB: %+v ok=%v", decoded, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedXuguSpatialBytesFallBack(t *testing.T) {
|
||||
value, srid, recognized := decodeXuguSpatialValue(mustDecodeXuguHex(t, "0101000020E6100000C520B072"))
|
||||
if value != "0x0101000020e6100000c520b072" || srid != nil || recognized {
|
||||
t.Fatalf("unexpected malformed fallback: value=%v srid=%v recognized=%v", value, srid, recognized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguDataTypesExposeSpatialDomainTypes(t *testing.T) {
|
||||
want := []string{"GEOMETRY", "GEOGRAPHY", "BOX2D", "BOX3D"}
|
||||
set := make(map[string]bool, len(xuguDataTypes))
|
||||
for _, value := range xuguDataTypes {
|
||||
set[value] = true
|
||||
}
|
||||
for _, value := range want {
|
||||
if !set[value] {
|
||||
t.Fatalf("spatial data type %q is not exposed", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustDecodeXuguHex(t *testing.T, value string) []byte {
|
||||
t.Helper()
|
||||
decoded, err := hex.DecodeString(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
Reference in New Issue
Block a user