feat(spatial): add SRID-aware geometry previews
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user