feat(spatial): add SRID-aware geometry previews

This commit is contained in:
amwps290 authored and GitHub committed 2026-07-31 22:31:51 +08:00
1 parent ee26b2a580
commit a6a072294a
57 files changed
+2771 -384

No files matched your search

@@ -81,6 +81,85 @@ public final class EwkbWktDecoder {
return value.toString();
}
/** Decode a geometry value while retaining SRID outside the display WKT. */
public static SpatialValue decodeSpatial(Object value) {
if (value == null) {
return new SpatialValue(null, null);
}
if (value instanceof byte[]) {
byte[] raw = (byte[]) value;
String wkt = decodeBytes(raw);
return new SpatialValue(wkt, wkt.startsWith("0x") ? null : topLevelSrid(raw));
}
String text;
if (value instanceof String) {
text = (String) value;
} else {
String pgValue = tryReflectiveGetValue(value);
text = pgValue == null ? value.toString() : pgValue;
}
SpatialValue ewkt = decodeEwkt(text);
if (ewkt != null) {
return ewkt;
}
byte[] raw = tryParseHex(text);
if (raw != null) {
String wkt = decodeBytes(raw);
return new SpatialValue(wkt, wkt.startsWith("0x") ? null : topLevelSrid(raw));
}
return new SpatialValue(text, null);
}
private static SpatialValue decodeEwkt(String value) {
if (value == null || value.length() < 7 || !value.regionMatches(true, 0, "SRID=", 0, 5)) {
return null;
}
int separator = value.indexOf(';', 5);
if (separator < 0) {
return null;
}
try {
int srid = Integer.parseInt(value.substring(5, separator));
return new SpatialValue(value.substring(separator + 1), srid <= 0 ? null : srid);
} catch (NumberFormatException ignored) {
return null;
}
}
private static Integer topLevelSrid(byte[] raw) {
if (raw == null || raw.length < 9) {
return null;
}
boolean little;
if (raw[0] == 0) {
little = false;
} else if (raw[0] == 1) {
little = true;
} else {
return null;
}
long typeWord = readU32(raw, 1, little);
if ((typeWord & 0x2000_0000L) == 0) {
return null;
}
long srid = readU32(raw, 5, little);
return srid == 0 || srid > Integer.MAX_VALUE ? null : (int) srid;
}
private static long readU32(byte[] raw, int offset, boolean little) {
if (little) {
return ((long) raw[offset] & 0xFF)
| (((long) raw[offset + 1] & 0xFF) << 8)
| (((long) raw[offset + 2] & 0xFF) << 16)
| (((long) raw[offset + 3] & 0xFF) << 24);
}
return (((long) raw[offset] & 0xFF) << 24)
| (((long) raw[offset + 1] & 0xFF) << 16)
| (((long) raw[offset + 2] & 0xFF) << 8)
| ((long) raw[offset + 3] & 0xFF);
}
/** Decode a raw EWKB byte sequence; returns hex {@code 0x..} on failure. */
static String decodeBytes(byte[] raw) {
if (raw == null) {
@@ -84,9 +84,9 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent {
if (isPostgisGeometryTypeName(columnTypeName)) {
Object raw = rs.getObject(index);
if (rs.wasNull() || raw == null) {
return null;
return new SpatialValue(null, null);
}
return EwkbWktDecoder.decode(raw);
return EwkbWktDecoder.decodeSpatial(raw);
}
return resultValue(rs, index, sqlType, columnTypeName);
};
@@ -7,6 +7,8 @@ import java.util.Objects;
public final class QueryPageResult {
private List<String> columns;
private List<String> column_types;
private List<SpatialColumn> spatial_columns;
private List<List<Integer>> spatial_values;
private List<List<Object>> rows;
private long affected_rows;
private long execution_time_ms;
@@ -56,7 +58,10 @@ public final class QueryPageResult {
) {
this.columns = columns == null ? Collections.emptyList() : columns;
this.column_types = column_types == null ? Collections.emptyList() : column_types;
this.rows = QueryResult.normalizeRows(rows);
QueryResult.NormalizedRows normalized = QueryResult.normalizeRowsWithSpatial(rows);
this.rows = normalized.rows;
this.spatial_columns = normalized.spatialColumns.isEmpty() ? null : normalized.spatialColumns;
this.spatial_values = normalized.spatialValues.isEmpty() ? null : normalized.spatialValues;
this.affected_rows = affected_rows;
this.execution_time_ms = execution_time_ms;
this.truncated = truncated;
@@ -76,6 +81,14 @@ public final class QueryPageResult {
return rows;
}
public List<SpatialColumn> getSpatial_columns() {
return spatial_columns == null ? Collections.emptyList() : spatial_columns;
}
public List<List<Integer>> getSpatial_values() {
return spatial_values == null ? Collections.emptyList() : spatial_values;
}
public long getAffected_rows() {
return affected_rows;
}
@@ -105,7 +118,18 @@ public final class QueryPageResult {
}
public void setRows(List<List<Object>> rows) {
this.rows = rows;
QueryResult.NormalizedRows normalized = QueryResult.normalizeRowsWithSpatial(rows);
this.rows = normalized.rows;
this.spatial_columns = normalized.spatialColumns.isEmpty() ? null : normalized.spatialColumns;
this.spatial_values = normalized.spatialValues.isEmpty() ? null : normalized.spatialValues;
}
public void setSpatial_columns(List<SpatialColumn> spatial_columns) {
this.spatial_columns = spatial_columns == null || spatial_columns.isEmpty() ? null : spatial_columns;
}
public void setSpatial_values(List<List<Integer>> spatial_values) {
this.spatial_values = spatial_values == null || spatial_values.isEmpty() ? null : spatial_values;
}
public void setAffected_rows(long affected_rows) {
@@ -139,19 +163,23 @@ public final class QueryPageResult {
&& has_more == that.has_more
&& Objects.equals(columns, that.columns)
&& Objects.equals(column_types, that.column_types)
&& Objects.equals(spatial_columns, that.spatial_columns)
&& Objects.equals(spatial_values, that.spatial_values)
&& Objects.equals(rows, that.rows)
&& Objects.equals(session_id, that.session_id);
}
@Override
public int hashCode() {
return Objects.hash(columns, column_types, rows, affected_rows, execution_time_ms, truncated, session_id, has_more);
return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, truncated, session_id, has_more);
}
@Override
public String toString() {
return "QueryPageResult(columns=" + columns
+ ", column_types=" + column_types
+ ", spatial_columns=" + spatial_columns
+ ", spatial_values=" + spatial_values
+ ", rows=" + rows
+ ", affected_rows=" + affected_rows
+ ", execution_time_ms=" + execution_time_ms
@@ -3,11 +3,17 @@ package com.dbx.agent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public final class QueryResult {
private List<String> columns;
private List<String> column_types;
private List<SpatialColumn> spatial_columns;
private List<List<Integer>> spatial_values;
private List<List<Object>> rows;
private long affected_rows;
private long execution_time_ms;
@@ -41,7 +47,10 @@ public final class QueryResult {
) {
this.columns = columns == null ? Collections.emptyList() : columns;
this.column_types = column_types == null ? Collections.emptyList() : column_types;
this.rows = normalizeRows(rows);
NormalizedRows normalized = normalizeRowsWithSpatial(rows);
this.rows = normalized.rows;
this.spatial_columns = normalized.spatialColumns.isEmpty() ? null : normalized.spatialColumns;
this.spatial_values = normalized.spatialValues.isEmpty() ? null : normalized.spatialValues;
this.affected_rows = affected_rows;
this.execution_time_ms = execution_time_ms;
this.truncated = truncated;
@@ -59,6 +68,14 @@ public final class QueryResult {
return rows;
}
public List<SpatialColumn> getSpatial_columns() {
return spatial_columns == null ? Collections.emptyList() : spatial_columns;
}
public List<List<Integer>> getSpatial_values() {
return spatial_values == null ? Collections.emptyList() : spatial_values;
}
public long getAffected_rows() {
return affected_rows;
}
@@ -80,7 +97,18 @@ public final class QueryResult {
}
public void setRows(List<List<Object>> rows) {
this.rows = rows;
NormalizedRows normalized = normalizeRowsWithSpatial(rows);
this.rows = normalized.rows;
this.spatial_columns = normalized.spatialColumns.isEmpty() ? null : normalized.spatialColumns;
this.spatial_values = normalized.spatialValues.isEmpty() ? null : normalized.spatialValues;
}
public void setSpatial_columns(List<SpatialColumn> spatial_columns) {
this.spatial_columns = spatial_columns == null || spatial_columns.isEmpty() ? null : spatial_columns;
}
public void setSpatial_values(List<List<Integer>> spatial_values) {
this.spatial_values = spatial_values == null || spatial_values.isEmpty() ? null : spatial_values;
}
public void setAffected_rows(long affected_rows) {
@@ -96,14 +124,63 @@ public final class QueryResult {
}
static List<List<Object>> normalizeRows(List<? extends List<?>> input) {
return normalizeRowsWithSpatial(input).rows;
}
static NormalizedRows normalizeRowsWithSpatial(List<? extends List<?>> input) {
if (input == null) {
return Collections.emptyList();
return new NormalizedRows(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
List<List<Object>> normalized = new ArrayList<>(input.size());
for (List<?> row : input) {
normalized.add(row == null ? Collections.emptyList() : new ArrayList<Object>(row));
List<List<Integer>> spatialValues = new ArrayList<>(input.size());
// column_index -> first non-null SRID seen (TreeMap keeps output ordered)
Map<Integer, Integer> sridByColumn = new TreeMap<>();
Set<Integer> spatialColumnIndexes = new TreeSet<>();
for (List<?> inputRow : input) {
List<?> row = inputRow == null ? Collections.emptyList() : inputRow;
List<Object> values = new ArrayList<>(row.size());
List<Integer> rowSrids = new ArrayList<>(row.size());
for (int columnIndex = 0; columnIndex < row.size(); columnIndex++) {
Object value = row.get(columnIndex);
if (value instanceof SpatialValue) {
SpatialValue spatialValue = (SpatialValue) value;
spatialColumnIndexes.add(columnIndex);
Integer srid = spatialValue.getSrid();
// SRID 0 means unknown, mirroring the Rust drivers.
rowSrids.add(srid == null || srid == 0 ? null : srid);
if (srid != null && srid != 0 && !sridByColumn.containsKey(columnIndex)) {
sridByColumn.put(columnIndex, srid);
}
values.add(spatialValue.getWkt());
} else {
rowSrids.add(null);
values.add(value);
}
}
normalized.add(values);
spatialValues.add(rowSrids);
}
List<SpatialColumn> spatialColumns = new ArrayList<>(spatialColumnIndexes.size());
for (Integer columnIndex : spatialColumnIndexes) {
spatialColumns.add(new SpatialColumn(columnIndex, sridByColumn.get(columnIndex)));
}
return new NormalizedRows(
normalized,
spatialColumns,
spatialColumns.isEmpty() ? Collections.emptyList() : spatialValues
);
}
static final class NormalizedRows {
final List<List<Object>> rows;
final List<SpatialColumn> spatialColumns;
final List<List<Integer>> spatialValues;
NormalizedRows(List<List<Object>> rows, List<SpatialColumn> spatialColumns, List<List<Integer>> spatialValues) {
this.rows = rows;
this.spatialColumns = spatialColumns;
this.spatialValues = spatialValues;
}
return normalized;
}
@Override
@@ -116,18 +193,22 @@ public final class QueryResult {
&& truncated == that.truncated
&& Objects.equals(columns, that.columns)
&& Objects.equals(column_types, that.column_types)
&& Objects.equals(spatial_columns, that.spatial_columns)
&& Objects.equals(spatial_values, that.spatial_values)
&& Objects.equals(rows, that.rows);
}
@Override
public int hashCode() {
return Objects.hash(columns, column_types, rows, affected_rows, execution_time_ms, truncated);
return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, truncated);
}
@Override
public String toString() {
return "QueryResult(columns=" + columns
+ ", column_types=" + column_types
+ ", spatial_columns=" + spatial_columns
+ ", spatial_values=" + spatial_values
+ ", rows=" + rows
+ ", affected_rows=" + affected_rows
+ ", execution_time_ms=" + execution_time_ms
@@ -0,0 +1,39 @@
package com.dbx.agent;
import java.util.Objects;
public final class SpatialColumn {
private final int column_index;
private final Integer srid;
public SpatialColumn(int columnIndex, Integer srid) {
this.column_index = columnIndex;
this.srid = srid;
}
public int getColumn_index() {
return column_index;
}
public Integer getSrid() {
return srid;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof SpatialColumn)) return false;
SpatialColumn that = (SpatialColumn) other;
return column_index == that.column_index && Objects.equals(srid, that.srid);
}
@Override
public int hashCode() {
return Objects.hash(column_index, srid);
}
@Override
public String toString() {
return "SpatialColumn(column_index=" + column_index + ", srid=" + srid + ")";
}
}
@@ -0,0 +1,20 @@
package com.dbx.agent;
/** Internal JDBC result value carrying display WKT and its optional SRID. */
public final class SpatialValue {
private final String wkt;
private final Integer srid;
public SpatialValue(String wkt, Integer srid) {
this.wkt = wkt;
this.srid = srid;
}
public String getWkt() {
return wkt;
}
public Integer getSrid() {
return srid;
}
}