99package org .locationtech .geomesa .trino .datastore ;
1010
1111import org .geotools .api .feature .simple .SimpleFeatureType ;
12+ import org .geotools .api .feature .type .AttributeDescriptor ;
13+ import org .geotools .api .feature .type .GeometryDescriptor ;
1214import org .geotools .feature .simple .SimpleFeatureTypeBuilder ;
15+ import org .locationtech .geomesa .utils .geotools .SimpleFeatureTypes ;
16+ import org .locationtech .jts .geom .Geometry ;
17+ import org .locationtech .jts .geom .Point ;
18+ import org .slf4j .Logger ;
19+ import org .slf4j .LoggerFactory ;
1320
1421import java .io .IOException ;
1522import java .sql .*;
1623import java .util .*;
1724
25+ import static org .locationtech .geomesa .trino .datastore .TrinoDataStore .escapeQuotes ;
26+
1827class TrinoSchemaDiscovery {
1928
29+ private static final Logger LOG = LoggerFactory .getLogger (TrinoSchemaDiscovery .class );
30+
31+ /** Iceberg table property holding the GeoMesa-encoded SimpleFeatureType spec. */
32+ static final String SFT_SPEC_PROPERTY = "geomesa.sft.spec" ;
33+
34+ /** Iceberg table property holding the GeoMesa type name. */
35+ static final String SFT_NAME_PROPERTY = "geomesa.sft.name" ;
36+
2037 private final TrinoDataStore store ;
2138
2239 TrinoSchemaDiscovery (TrinoDataStore store ) {
@@ -28,8 +45,17 @@ SimpleFeatureType discover(String typeName) throws IOException {
2845 tb .setName (typeName );
2946 tb .setNamespaceURI (store .getNamespaceURI ());
3047
31- String sql = String .format ("SELECT * FROM \" %s\" .\" %s\" .\" %s\" LIMIT 0" ,
32- store .catalog (), store .trinoSchema (), typeName );
48+ String sql = String .format (
49+ "SELECT * FROM %s.%s.%s LIMIT 0" ,
50+ escapeQuotes (store .catalog ()),
51+ escapeQuotes (store .trinoSchema ()),
52+ escapeQuotes (typeName )
53+ );
54+
55+ Map <String , String > sftProps = readSftProperties (typeName );
56+ String sftSpec = sftProps .get (SFT_SPEC_PROPERTY );
57+ Map <String , Class <?>> sftBindings = (sftSpec == null || sftSpec .isBlank ())
58+ ? Map .of () : geometryBindingsFromSpec (typeName , sftSpec );
3359
3460 String visColumn = null ;
3561 try (Connection conn = store .connect ();
@@ -54,16 +80,14 @@ SimpleFeatureType discover(String typeName) throws IOException {
5480 for (int i = 1 ; i <= meta .getColumnCount (); i ++) {
5581 String name = meta .getColumnName (i );
5682 if (TrinoTypeMapper .isHidden (name )) continue ;
57- if (name .equals (visColumn )) continue ; // vis column is metadata, not a SFT attribute
83+ if (name .equals (visColumn )) continue ; // vis column is metadata, not an SFT attribute
5884
5985 boolean isGeom = geometryColumnNames .contains (name );
60- // A __X_z2__ companion marks a point-only geometry column, so the
61- // attribute can be bound to Point rather than the generic Geometry.
62- boolean isPoint = isGeom && isPointColumn (name , allNames );
86+ Class <?> geomBinding = isGeom ? resolveGeometryBinding (name , allNames , sftBindings ) : null ;
6387 // SRID is no longer carried in a table property; default to WGS84.
6488 int srid = isGeom ? 4326 : 0 ;
6589 var descriptor =
66- TrinoTypeMapper .toDescriptor (name , meta .getColumnType (i ), isGeom , isPoint , srid );
90+ TrinoTypeMapper .toDescriptor (name , meta .getColumnType (i ), isGeom , geomBinding , srid );
6791 tb .add (descriptor );
6892
6993 if (isGeom && !defaultGeomSet ) {
@@ -79,9 +103,70 @@ SimpleFeatureType discover(String typeName) throws IOException {
79103 if (visColumn != null ) {
80104 sft .getUserData ().put (VIS_COLUMN_KEY , visColumn );
81105 }
106+ String sftName = sftProps .get (SFT_NAME_PROPERTY );
107+ if (sftName != null && !sftName .isBlank ()) {
108+ sft .getUserData ().put (SFT_NAME_PROPERTY , sftName );
109+ }
82110 return sft ;
83111 }
84112
113+ /** The JTS geometry binding for a geometry column: the subtype declared by the stored SFT
114+ * when it names this attribute, else {@link Point} for a {@code __<name>_z2__} companion,
115+ * generic {@link Geometry} otherwise. */
116+ static Class <?> resolveGeometryBinding (String name , Set <String > allNames ,
117+ Map <String , Class <?>> sftBindings ) {
118+ Class <?> fromSft = sftBindings .get (name );
119+ if (fromSft != null ) {
120+ return fromSft ;
121+ }
122+ return isPointColumn (name , allNames ) ? Point .class : Geometry .class ;
123+ }
124+
125+ /** Parses a GeoMesa SFT spec into geometry-attribute-name → JTS subtype. */
126+ static Map <String , Class <?>> geometryBindingsFromSpec (String typeName , String spec ) {
127+ try {
128+ SimpleFeatureType sft = SimpleFeatureTypes .createType (typeName , spec );
129+ Map <String , Class <?>> bindings = new LinkedHashMap <>();
130+ for (AttributeDescriptor d : sft .getAttributeDescriptors ()) {
131+ if (d instanceof GeometryDescriptor ) {
132+ bindings .put (d .getLocalName (), d .getType ().getBinding ());
133+ }
134+ }
135+ return bindings ;
136+ } catch (RuntimeException e ) {
137+ LOG .warn ("Could not parse stored GeoMesa SFT for '{}' ({}='{}'): {}; "
138+ + "falling back to heuristic geometry binding" ,
139+ typeName , SFT_SPEC_PROPERTY , spec , e .getMessage ());
140+ return Map .of ();
141+ }
142+ }
143+
144+ /** Reads the {@code geomesa.sft.*} properties (spec + name) from the Iceberg
145+ * {@code <table>$properties} metadata table in one query; empty when the table carries none
146+ * or doesn't expose {@code $properties}. Non-fatal: any failure just disables SFT-driven
147+ * binding/name for this table. */
148+ private Map <String , String > readSftProperties (String typeName ) {
149+ String sql = String .format (
150+ "SELECT key, value FROM %s.%s.%s WHERE key IN ('%s', '%s')" ,
151+ escapeQuotes (store .catalog ()),
152+ escapeQuotes (store .trinoSchema ()),
153+ escapeQuotes (typeName + "$properties" ),
154+ SFT_SPEC_PROPERTY , SFT_NAME_PROPERTY
155+ );
156+ Map <String , String > props = new HashMap <>();
157+ try (Connection conn = store .connect ();
158+ Statement stmt = conn .createStatement ();
159+ ResultSet rs = stmt .executeQuery (sql )) {
160+ while (rs .next ()) {
161+ props .put (rs .getString (1 ), rs .getString (2 ));
162+ }
163+ } catch (SQLException e ) {
164+ LOG .debug ("No readable $properties for '{}' (no SFT metadata): {}" ,
165+ typeName , e .getMessage ());
166+ }
167+ return props ;
168+ }
169+
85170 /** Returns the set of column names that are geometry columns under the
86171 * naming convention: a base name {@code X} is a geometry column iff at least
87172 * one of {@code __X_bbox__}, {@code __X_z2__}, {@code __X_xz2__} appears in
0 commit comments