activerecord-trilogis-adapter 8.1.1 → 8.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_record/connection_adapters/trilogis/arel_tosql.rb +19 -40
- data/lib/active_record/connection_adapters/trilogis/schema_creation.rb +1 -12
- data/lib/active_record/connection_adapters/trilogis/schema_statements.rb +31 -187
- data/lib/active_record/connection_adapters/trilogis/spatial_column.rb +30 -5
- data/lib/active_record/connection_adapters/trilogis/spatial_column_info.rb +5 -18
- data/lib/active_record/connection_adapters/trilogis/spatial_expressions.rb +5 -65
- data/lib/active_record/connection_adapters/trilogis/spatial_table_definition.rb +17 -23
- data/lib/active_record/connection_adapters/trilogis/version.rb +1 -1
- data/lib/active_record/connection_adapters/trilogis_adapter.rb +14 -152
- data/lib/active_record/type/spatial.rb +31 -126
- data/lib/activerecord-trilogis-adapter.rb +0 -7
- metadata +39 -26
- data/lib/active_record/connection_adapters/trilogis/railtie.rb +0 -30
- data/lib/active_record/dependency_loader.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cff233bec60dde0ec31a359eb0c3725de326e2bff707a9e8e20e7a59e96d2711
|
|
4
|
+
data.tar.gz: 32778c148a38f3a15bb6e32f64d6e89f734584fb2c9aea9756e213dea1c6ae68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9382cfbc137be0434e049c40a95d1dce30de00d8008e2f7eff81c18e0d2367bac4531b017f79e53127545abaa3667f298c55df5f40e115f9999dc88442dec0ec
|
|
7
|
+
data.tar.gz: 8504eaf6bb77294fc427f77556b2bf1291d2a81373c95238e6b8a14974614c329d51d9c4bed49e92fa436d283ce8873b03bc8bbea16b213e2f1c3de9c2c8907b
|
|
@@ -48,48 +48,36 @@ module Arel
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def visit_spatial_value(node, collector)
|
|
52
|
-
case node
|
|
53
|
-
when RGeo::Feature::Instance
|
|
54
|
-
visit_RGeo_Feature_Instance(node, collector)
|
|
55
|
-
when String
|
|
56
|
-
if node.match?(/^[A-Z]/) # WKT string
|
|
57
|
-
visit_wkt_string(node, collector)
|
|
58
|
-
else
|
|
59
|
-
super
|
|
60
|
-
end
|
|
61
|
-
else
|
|
62
|
-
super
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
51
|
def visit_RGeo_Feature_Instance(node, collector)
|
|
67
52
|
srid = node.srid || 0
|
|
68
53
|
wkt = node.as_text
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
append_geometry(wkt, srid, collector)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def visit_wkt_string(wkt, collector)
|
|
59
|
+
if (match = /\ASRID=(\d+);(.*)\z/im.match(wkt))
|
|
60
|
+
append_geometry(match[2], match[1].to_i, collector)
|
|
74
61
|
else
|
|
75
|
-
|
|
62
|
+
append_geometry(wkt, 0, collector)
|
|
76
63
|
end
|
|
77
64
|
end
|
|
78
65
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
def append_geometry(wkt, srid, collector)
|
|
67
|
+
collector << "ST_GeomFromText(#{quote(wkt)}, #{srid}, " \
|
|
68
|
+
"#{ActiveRecord::ConnectionAdapters::TrilogisAdapter::AXIS_ORDER_LONG_LAT})"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def visit_RGeo_ActiveRecord_SpatialNamedFunction(node, collector)
|
|
72
|
+
node = node.dup
|
|
73
|
+
node.expressions = node.expressions.each_with_index.map do |argument, index|
|
|
74
|
+
if node.spatial_argument?(index) || argument.is_a?(RGeo::ActiveRecord::SpatialConstantNode)
|
|
75
|
+
argument
|
|
87
76
|
else
|
|
88
|
-
|
|
77
|
+
Arel::Nodes.build_quoted(argument)
|
|
89
78
|
end
|
|
90
|
-
else
|
|
91
|
-
collector << "ST_GeomFromText('#{wkt}', 0)"
|
|
92
79
|
end
|
|
80
|
+
super
|
|
93
81
|
end
|
|
94
82
|
|
|
95
83
|
# Handle spatial function calls
|
|
@@ -140,15 +128,6 @@ module Arel
|
|
|
140
128
|
collector << quote(value)
|
|
141
129
|
end
|
|
142
130
|
end
|
|
143
|
-
|
|
144
|
-
# Support for spatial predicates in WHERE clauses
|
|
145
|
-
def visit_spatial_predicate(predicate_name, left, right, collector)
|
|
146
|
-
collector << "#{SPATIAL_FUNCTIONS[predicate_name]}("
|
|
147
|
-
visit(left, collector)
|
|
148
|
-
collector << ", "
|
|
149
|
-
visit(right, collector)
|
|
150
|
-
collector << ")"
|
|
151
|
-
end
|
|
152
131
|
end
|
|
153
132
|
end
|
|
154
133
|
end
|
|
@@ -7,18 +7,7 @@ module ActiveRecord
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def add_column_options!(sql, options)
|
|
10
|
-
|
|
11
|
-
# Format: /*!80003 SRID #{srid} */
|
|
12
|
-
if options[:srid]
|
|
13
|
-
sql_result = "#{sql} /*!80003 SRID #{options[:srid]} */"
|
|
14
|
-
sql.replace(sql_result)
|
|
15
|
-
|
|
16
|
-
# MySQL does not support DEFAULT values for spatial columns
|
|
17
|
-
# Remove :default option before calling super to prevent SQL errors
|
|
18
|
-
options = options.dup
|
|
19
|
-
options.delete(:default)
|
|
20
|
-
end
|
|
21
|
-
|
|
10
|
+
sql << " SRID #{Integer(options[:srid])}" if options[:srid]
|
|
22
11
|
super
|
|
23
12
|
end
|
|
24
13
|
end
|
|
@@ -5,215 +5,59 @@ module ActiveRecord
|
|
|
5
5
|
module Trilogis
|
|
6
6
|
module SchemaStatements
|
|
7
7
|
def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
end
|
|
16
|
-
sql_type = spatial_sql_type(base_type, nil)
|
|
17
|
-
sql_type = "#{sql_type} SRID #{limit[:srid]}" if limit.is_a?(Hash) && limit[:srid]
|
|
18
|
-
sql_type
|
|
19
|
-
else
|
|
20
|
-
super
|
|
21
|
-
end
|
|
8
|
+
return super unless (spatial_type = TrilogisAdapter.spatial_type(type))
|
|
9
|
+
|
|
10
|
+
spatial_type = TrilogisAdapter.spatial_type(limit[:type]) if limit.is_a?(Hash) && limit[:type]
|
|
11
|
+
sql = spatial_type.upcase
|
|
12
|
+
sql += " SRID #{Integer(limit[:srid])}" if limit.is_a?(Hash) && limit[:srid]
|
|
13
|
+
sql
|
|
22
14
|
end
|
|
23
15
|
|
|
24
16
|
def add_index(table_name, column_name, **options)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Handle spatial indexes - MySQL uses SPATIAL keyword, not USING
|
|
28
|
-
if index_type == :spatial
|
|
29
|
-
options = options.dup
|
|
30
|
-
options.delete(:using) # Remove any USING clause for spatial indexes
|
|
31
|
-
options[:type] = :spatial
|
|
32
|
-
end
|
|
33
|
-
|
|
17
|
+
options.delete(:using) if options[:type] == :spatial
|
|
34
18
|
super
|
|
35
19
|
end
|
|
36
20
|
|
|
37
21
|
def indexes(table_name)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
indexes.each do |index|
|
|
42
|
-
if index.using == :gist || index.comment&.include?("spatial") || index.type == :spatial
|
|
43
|
-
index.instance_variable_set(:@lengths, {})
|
|
44
|
-
end
|
|
22
|
+
super.each do |index|
|
|
23
|
+
# MySQL reports a prefix length for SPATIAL keys that cannot be reloaded.
|
|
24
|
+
index.instance_variable_set(:@lengths, {}) if index.type == :spatial
|
|
45
25
|
end
|
|
46
|
-
|
|
47
|
-
indexes
|
|
48
26
|
end
|
|
49
27
|
|
|
50
|
-
|
|
51
|
-
|
|
28
|
+
def columns(table_name)
|
|
29
|
+
previous_metadata = @spatial_column_metadata
|
|
30
|
+
@spatial_column_metadata = nil
|
|
31
|
+
super
|
|
32
|
+
ensure
|
|
33
|
+
@spatial_column_metadata = previous_metadata
|
|
34
|
+
end
|
|
52
35
|
|
|
53
|
-
# Override to properly handle spatial columns creation
|
|
54
36
|
def new_column_from_field(table_name, field, definitions = nil)
|
|
55
|
-
|
|
56
|
-
sql_type = extract_field_value(field, :Type, :type)
|
|
37
|
+
return super unless (spatial_type = TrilogisAdapter.spatial_type(field["Type"]))
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
39
|
+
@spatial_column_metadata ||= SpatialColumnInfo.new(self, table_name).all
|
|
40
|
+
info = @spatial_column_metadata[field["Field"]]
|
|
41
|
+
cast_type = Type::Spatial.new(info ? "geometry(#{info[:type]},#{info[:srid] || 0})" : spatial_type).freeze
|
|
42
|
+
SpatialColumn.new(
|
|
43
|
+
field["Field"], cast_type,
|
|
44
|
+
nil, fetch_type_metadata(field["Type"]), field["Null"] == "YES", nil,
|
|
45
|
+
collation: field["Collation"], comment: field["Comment"].presence, spatial_info: info
|
|
46
|
+
)
|
|
63
47
|
end
|
|
64
48
|
|
|
65
49
|
def create_table_definition(name, **)
|
|
66
50
|
Trilogis::TableDefinition.new(self, name, **)
|
|
67
51
|
end
|
|
68
52
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def add_column(table_name, column_name, type, **options)
|
|
77
|
-
if spatial_type?(type.to_s)
|
|
78
|
-
# Build ALTER TABLE statement for spatial column
|
|
79
|
-
sql_type = spatial_sql_type(type, options[:type])
|
|
80
|
-
base_sql = "ALTER TABLE #{quote_table_name(table_name)} " \
|
|
81
|
-
"ADD #{quote_column_name(column_name)} #{sql_type}"
|
|
82
|
-
sql_parts = [base_sql]
|
|
83
|
-
|
|
84
|
-
# Add SRID if specified
|
|
85
|
-
sql_parts << " SRID #{options[:srid]}" if options[:srid] && options[:srid] != 0
|
|
86
|
-
|
|
87
|
-
# Add NULL constraint
|
|
88
|
-
sql_parts << " NOT NULL" if options[:null] == false
|
|
89
|
-
|
|
90
|
-
# MySQL does not support DEFAULT values for spatial/geometry columns
|
|
91
|
-
# Silently ignore :default option to prevent SQL syntax errors
|
|
92
|
-
# Users should handle defaults at application level instead
|
|
93
|
-
|
|
94
|
-
execute sql_parts.join
|
|
95
|
-
|
|
96
|
-
# Clear memoized spatial column info for this table
|
|
97
|
-
clear_spatial_cache_for(table_name)
|
|
98
|
-
else
|
|
99
|
-
super
|
|
53
|
+
def build_change_column_definition(table_name, column_name, type, **options)
|
|
54
|
+
column = column_for(table_name, column_name)
|
|
55
|
+
if column.spatial? && (type.nil? || TrilogisAdapter.spatial_type(type))
|
|
56
|
+
type ||= column.type
|
|
57
|
+
options[:srid] = column.srid_constraint unless options.key?(:srid) || options[:limit].is_a?(Hash)
|
|
100
58
|
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def drop_table(table_name, **options)
|
|
104
|
-
# Clear memoized spatial column info for this table before dropping
|
|
105
|
-
clear_spatial_cache_for(table_name)
|
|
106
59
|
super
|
|
107
60
|
end
|
|
108
|
-
|
|
109
|
-
def rename_table(table_name, new_name)
|
|
110
|
-
# Clear cache for both old and new table names
|
|
111
|
-
clear_spatial_cache_for(table_name)
|
|
112
|
-
clear_spatial_cache_for(new_name)
|
|
113
|
-
super
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Clear all spatial column caches (useful for tests)
|
|
117
|
-
def clear_spatial_cache!
|
|
118
|
-
@spatial_column_info = {}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def spatial_sql_type(base_type, subtype = nil)
|
|
122
|
-
sql_type = base_type.to_s.delete("_").upcase
|
|
123
|
-
subtype_sql = subtype.to_s
|
|
124
|
-
if subtype_sql.empty?
|
|
125
|
-
sql_type
|
|
126
|
-
else
|
|
127
|
-
"#{sql_type}(#{subtype_sql.delete('_').upcase})"
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def spatial_type?(type)
|
|
134
|
-
TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(type.to_s.downcase)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def schema_creation
|
|
138
|
-
SchemaCreation.new(self)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Memoized spatial column info per table
|
|
142
|
-
def spatial_column_info(table_name)
|
|
143
|
-
@spatial_column_info ||= {}
|
|
144
|
-
@spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Clear spatial cache for a specific table
|
|
148
|
-
def clear_spatial_cache_for(table_name)
|
|
149
|
-
@spatial_column_info&.delete(table_name.to_sym)
|
|
150
|
-
@spatial_column_info&.delete(table_name.to_s)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Extract field value with case-insensitive key lookup
|
|
154
|
-
def extract_field_value(field, *keys)
|
|
155
|
-
keys.each do |key|
|
|
156
|
-
return field[key] if field.key?(key)
|
|
157
|
-
return field[key.to_s] if field.key?(key.to_s)
|
|
158
|
-
end
|
|
159
|
-
nil
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Build a spatial column from field metadata
|
|
163
|
-
def build_spatial_column(table_name, field, field_name, sql_type)
|
|
164
|
-
spatial_info = spatial_column_info(table_name).get(field_name, sql_type)
|
|
165
|
-
|
|
166
|
-
# Keep original sql_type in metadata (point, linestring, etc.)
|
|
167
|
-
# This preserves the actual geometric type for @geo_type_name
|
|
168
|
-
type_metadata = fetch_type_metadata(sql_type)
|
|
169
|
-
|
|
170
|
-
# Lookup cast type as "geometry" for schema dumper compatibility
|
|
171
|
-
# Schema dumper validates cast_type, and only "geometry" is registered as table method
|
|
172
|
-
# Actual type is preserved in sql_type_metadata and limit hash
|
|
173
|
-
cast_type = lookup_cast_type("geometry")
|
|
174
|
-
|
|
175
|
-
SpatialColumn.new(
|
|
176
|
-
field_name,
|
|
177
|
-
cast_type,
|
|
178
|
-
nil, # MySQL spatial columns cannot have DEFAULT values
|
|
179
|
-
type_metadata,
|
|
180
|
-
extract_field_value(field, :Null, :null) == "YES",
|
|
181
|
-
nil, # MySQL spatial columns cannot have DEFAULT functions
|
|
182
|
-
collation: extract_field_value(field, :Collation, :collation),
|
|
183
|
-
comment: extract_field_value(field, :Comment, :comment).presence,
|
|
184
|
-
spatial_info: spatial_info
|
|
185
|
-
)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
|
|
190
|
-
private
|
|
191
|
-
|
|
192
|
-
def visit_ColumnDefinition(o)
|
|
193
|
-
if spatial_column?(o)
|
|
194
|
-
sql_type = spatial_sql_type(o.sql_type, o.options[:type])
|
|
195
|
-
column_sql_parts = ["#{quote_column_name(o.name)} #{sql_type}"]
|
|
196
|
-
|
|
197
|
-
# Add SRID if specified (MySQL 8.0+ syntax: COLUMN TYPE SRID value)
|
|
198
|
-
column_sql_parts << " SRID #{o.options[:srid]}" if o.options[:srid] && o.options[:srid] != 0
|
|
199
|
-
|
|
200
|
-
column_sql_parts << " NOT NULL" unless o.null
|
|
201
|
-
|
|
202
|
-
# MySQL does not support DEFAULT values for spatial/geometry columns
|
|
203
|
-
# Silently ignore default to prevent SQL syntax errors
|
|
204
|
-
|
|
205
|
-
column_sql_parts.join
|
|
206
|
-
else
|
|
207
|
-
super
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def spatial_column?(column)
|
|
212
|
-
# Check both with and without underscores
|
|
213
|
-
sql_type = column.sql_type.to_s.downcase
|
|
214
|
-
TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type) ||
|
|
215
|
-
TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type.delete("_"))
|
|
216
|
-
end
|
|
217
61
|
end
|
|
218
62
|
end
|
|
219
63
|
end
|
|
@@ -13,7 +13,7 @@ module ActiveRecord
|
|
|
13
13
|
|
|
14
14
|
module Trilogis
|
|
15
15
|
class SpatialColumn < ActiveRecord::ConnectionAdapters::MySQL::Column
|
|
16
|
-
attr_reader :geometric_type, :srid, :geo_type_name
|
|
16
|
+
attr_reader :geometric_type, :srid, :geo_type_name, :srid_constraint
|
|
17
17
|
|
|
18
18
|
# Map SQL type strings to RGeo type classes
|
|
19
19
|
GEOMETRIC_TYPES = {
|
|
@@ -39,12 +39,13 @@ module ActiveRecord
|
|
|
39
39
|
# Use spatial info from INFORMATION_SCHEMA if available
|
|
40
40
|
@geo_type_name = spatial_info[:type].to_s.downcase
|
|
41
41
|
@geometric_type = GEOMETRIC_TYPES[@geo_type_name] || RGeo::Feature::Geometry
|
|
42
|
-
@
|
|
42
|
+
@srid_constraint = spatial_info[:srid]
|
|
43
|
+
@srid = @srid_constraint || 0
|
|
43
44
|
@has_z = spatial_info[:has_z] || false
|
|
44
45
|
@has_m = spatial_info[:has_m] || false
|
|
45
46
|
else
|
|
46
47
|
# Fallback to extracting from SQL type
|
|
47
|
-
@geo_type_name = sql_type_metadata.sql_type
|
|
48
|
+
@geo_type_name = TrilogisAdapter.spatial_type(sql_type_metadata.sql_type)
|
|
48
49
|
type_info = Type::Spatial.new(@geo_type_name)
|
|
49
50
|
@geometric_type = GEOMETRIC_TYPES[@geo_type_name] || RGeo::Feature::Geometry
|
|
50
51
|
@srid = type_info.srid || 0
|
|
@@ -57,7 +58,7 @@ module ActiveRecord
|
|
|
57
58
|
# Guard against nil sql_type_metadata during type registration
|
|
58
59
|
return false unless sql_type_metadata&.sql_type
|
|
59
60
|
|
|
60
|
-
TrilogisAdapter
|
|
61
|
+
!TrilogisAdapter.spatial_type(sql_type_metadata.sql_type).nil?
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def has_z?
|
|
@@ -86,7 +87,31 @@ module ActiveRecord
|
|
|
86
87
|
|
|
87
88
|
# Only include SRID in limit
|
|
88
89
|
# Type information is in the type() method
|
|
89
|
-
{ srid: @
|
|
90
|
+
{ srid: @srid_constraint } unless @srid_constraint.nil?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ==(other)
|
|
94
|
+
super && other.is_a?(self.class) &&
|
|
95
|
+
srid_constraint == other.srid_constraint && geo_type_name == other.geo_type_name
|
|
96
|
+
end
|
|
97
|
+
alias eql? ==
|
|
98
|
+
|
|
99
|
+
def hash
|
|
100
|
+
[super, srid_constraint, geo_type_name].hash
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def encode_with(coder)
|
|
104
|
+
super
|
|
105
|
+
coder["srid_constraint"] = @srid_constraint
|
|
106
|
+
coder["geo_type_name"] = @geo_type_name
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def init_with(coder)
|
|
110
|
+
super
|
|
111
|
+
@srid_constraint = coder["srid_constraint"]
|
|
112
|
+
@srid = @srid_constraint || 0
|
|
113
|
+
@geo_type_name = coder["geo_type_name"]
|
|
114
|
+
@geometric_type = GEOMETRIC_TYPES.fetch(@geo_type_name, RGeo::Feature::Geometry)
|
|
90
115
|
end
|
|
91
116
|
|
|
92
117
|
# Override default to always return nil for spatial columns
|
|
@@ -4,7 +4,6 @@ module ActiveRecord
|
|
|
4
4
|
module ConnectionAdapters
|
|
5
5
|
module Trilogis
|
|
6
6
|
# Queries MySQL INFORMATION_SCHEMA for spatial column metadata
|
|
7
|
-
# and caches the results for performance
|
|
8
7
|
class SpatialColumnInfo
|
|
9
8
|
def initialize(adapter, table_name)
|
|
10
9
|
@adapter = adapter
|
|
@@ -12,49 +11,37 @@ module ActiveRecord
|
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def all
|
|
14
|
+
scope = @adapter.send(:quoted_scope, @table_name)
|
|
15
15
|
# Query MySQL's ST_GEOMETRY_COLUMNS view for SRID info
|
|
16
16
|
# This view properly returns SRS_ID for spatial columns
|
|
17
17
|
sql = <<~SQL.squish
|
|
18
18
|
SELECT
|
|
19
19
|
gc.COLUMN_NAME as column_name,
|
|
20
20
|
gc.SRS_ID as srs_id,
|
|
21
|
-
gc.GEOMETRY_TYPE_NAME as geometry_type,
|
|
22
21
|
c.COLUMN_TYPE as column_type
|
|
23
22
|
FROM INFORMATION_SCHEMA.ST_GEOMETRY_COLUMNS gc
|
|
24
23
|
JOIN INFORMATION_SCHEMA.COLUMNS c
|
|
25
24
|
ON gc.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
26
25
|
AND gc.TABLE_NAME = c.TABLE_NAME
|
|
27
26
|
AND gc.COLUMN_NAME = c.COLUMN_NAME
|
|
28
|
-
WHERE gc.TABLE_SCHEMA =
|
|
29
|
-
AND gc.TABLE_NAME = #{
|
|
27
|
+
WHERE gc.TABLE_SCHEMA = #{scope[:schema]}
|
|
28
|
+
AND gc.TABLE_NAME = #{scope[:name]}
|
|
30
29
|
SQL
|
|
31
30
|
|
|
32
31
|
result = {}
|
|
33
32
|
@adapter.exec_query(sql, "SCHEMA").each do |row|
|
|
34
33
|
column_name = row["column_name"] || row["COLUMN_NAME"]
|
|
35
34
|
srs_id = row["srs_id"] || row["SRS_ID"]
|
|
36
|
-
row["geometry_type"] || row["GEOMETRY_TYPE"]
|
|
37
35
|
column_type = (row["column_type"] || row["COLUMN_TYPE"]).to_s.sub(/m$/, "")
|
|
38
36
|
|
|
39
37
|
result[column_name] = {
|
|
40
38
|
name: column_name,
|
|
41
|
-
srid: srs_id
|
|
42
|
-
type: column_type
|
|
39
|
+
srid: srs_id&.to_i,
|
|
40
|
+
type: TrilogisAdapter.spatial_type(column_type)
|
|
43
41
|
}
|
|
44
42
|
end
|
|
45
43
|
result
|
|
46
44
|
end
|
|
47
|
-
|
|
48
|
-
# Get spatial info for a specific column if it's spatial
|
|
49
|
-
# Returns nil for non-spatial columns to avoid unnecessary queries
|
|
50
|
-
def get(column_name, sql_type)
|
|
51
|
-
# Only query for known spatial types
|
|
52
|
-
return unless TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type.to_s.downcase)
|
|
53
|
-
|
|
54
|
-
# Don't memoize - always query fresh data to avoid stale cache issues
|
|
55
|
-
# when columns are added during tests
|
|
56
|
-
all[column_name]
|
|
57
|
-
end
|
|
58
45
|
end
|
|
59
46
|
end
|
|
60
47
|
end
|
|
@@ -5,77 +5,17 @@ module RGeo
|
|
|
5
5
|
module Trilogis
|
|
6
6
|
# MySQL-specific spatial expressions
|
|
7
7
|
module SpatialExpressions
|
|
8
|
+
# Keep Arel expressions intact; the upstream implementation coerces distance to Float.
|
|
9
|
+
def st_buffer(distance)
|
|
10
|
+
SpatialNamedFunction.new("ST_Buffer", [self, distance], [true, true, false])
|
|
11
|
+
end
|
|
12
|
+
|
|
8
13
|
# MySQL ST_Distance_Sphere for geographic distance calculations
|
|
9
14
|
def st_distance_sphere(rhs, units = nil)
|
|
10
15
|
args = [self, rhs]
|
|
11
16
|
args << units.to_s if units
|
|
12
17
|
SpatialNamedFunction.new("ST_Distance_Sphere", args, [false, true, true, false])
|
|
13
18
|
end
|
|
14
|
-
|
|
15
|
-
# Additional MySQL spatial functions
|
|
16
|
-
def st_buffer(distance)
|
|
17
|
-
SpatialNamedFunction.new("ST_Buffer", [self, distance], [false, true, false])
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def st_contains(rhs)
|
|
21
|
-
SpatialNamedFunction.new("ST_Contains", [self, rhs], [false, true, true])
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def st_within(rhs)
|
|
25
|
-
SpatialNamedFunction.new("ST_Within", [self, rhs], [false, true, true])
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def st_intersects(rhs)
|
|
29
|
-
SpatialNamedFunction.new("ST_Intersects", [self, rhs], [false, true, true])
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def st_crosses(rhs)
|
|
33
|
-
SpatialNamedFunction.new("ST_Crosses", [self, rhs], [false, true, true])
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def st_touches(rhs)
|
|
37
|
-
SpatialNamedFunction.new("ST_Touches", [self, rhs], [false, true, true])
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def st_overlaps(rhs)
|
|
41
|
-
SpatialNamedFunction.new("ST_Overlaps", [self, rhs], [false, true, true])
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def st_equals(rhs)
|
|
45
|
-
SpatialNamedFunction.new("ST_Equals", [self, rhs], [false, true, true])
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def st_disjoint(rhs)
|
|
49
|
-
SpatialNamedFunction.new("ST_Disjoint", [self, rhs], [false, true, true])
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def st_area
|
|
53
|
-
SpatialNamedFunction.new("ST_Area", [self], [false, true])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def st_length
|
|
57
|
-
SpatialNamedFunction.new("ST_Length", [self], [false, true])
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def st_centroid
|
|
61
|
-
SpatialNamedFunction.new("ST_Centroid", [self], [false, true])
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def st_envelope
|
|
65
|
-
SpatialNamedFunction.new("ST_Envelope", [self], [false, true])
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def st_astext
|
|
69
|
-
SpatialNamedFunction.new("ST_AsText", [self], [false, true])
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def st_asbinary
|
|
73
|
-
SpatialNamedFunction.new("ST_AsBinary", [self], [false, true])
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def st_srid
|
|
77
|
-
SpatialNamedFunction.new("ST_SRID", [self], [false, true])
|
|
78
|
-
end
|
|
79
19
|
end
|
|
80
20
|
end
|
|
81
21
|
end
|
|
@@ -53,36 +53,30 @@ module ActiveRecord
|
|
|
53
53
|
class TableDefinition < ActiveRecord::ConnectionAdapters::MySQL::TableDefinition
|
|
54
54
|
include ColumnMethods
|
|
55
55
|
|
|
56
|
-
# Override column to handle spatial types
|
|
57
56
|
def column(name, type, index: nil, **options)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
col.options[key] = value
|
|
57
|
+
index = { type: :spatial } if TrilogisAdapter.spatial_type(type) && [true, :spatial].include?(index)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def new_column_definition(name, type, **options)
|
|
62
|
+
if (spatial_type = TrilogisAdapter.spatial_type(type))
|
|
63
|
+
type = spatial_type.to_sym
|
|
64
|
+
# Retain legacy no-op options for existing migrations.
|
|
65
|
+
options.except!(:has_z, :has_m, :geographic)
|
|
66
|
+
options.delete(:default)
|
|
67
|
+
if options[:limit].is_a?(Hash)
|
|
68
|
+
limit = options.delete(:limit)
|
|
69
|
+
type = TrilogisAdapter.spatial_type(limit[:type]).to_sym if limit[:type]
|
|
70
|
+
options[:srid] = limit[:srid] unless options.key?(:srid)
|
|
73
71
|
end
|
|
74
72
|
end
|
|
75
|
-
|
|
76
|
-
# Add spatial index if requested
|
|
77
|
-
@indexes << [name, { type: :spatial }] if index && spatial_type?(type) && [true, :spatial].include?(index)
|
|
78
|
-
|
|
79
|
-
result
|
|
73
|
+
super
|
|
80
74
|
end
|
|
81
75
|
|
|
82
76
|
private
|
|
83
77
|
|
|
84
|
-
def
|
|
85
|
-
|
|
78
|
+
def valid_column_definition_options
|
|
79
|
+
super + [:srid]
|
|
86
80
|
end
|
|
87
81
|
end
|
|
88
82
|
end
|
|
@@ -15,48 +15,6 @@ require_relative "../type/spatial"
|
|
|
15
15
|
require_relative "../tasks/trilogis_database_tasks"
|
|
16
16
|
|
|
17
17
|
module ActiveRecord
|
|
18
|
-
module ConnectionHandling
|
|
19
|
-
# Establishes a connection to the database using the Trilogis adapter.
|
|
20
|
-
# This adapter extends the built-in Trilogy adapter with spatial support.
|
|
21
|
-
def trilogis_connection(config)
|
|
22
|
-
configuration = config.dup
|
|
23
|
-
|
|
24
|
-
# Ensure required configuration
|
|
25
|
-
configuration[:prepared_statements] = true unless configuration.key?(:prepared_statements)
|
|
26
|
-
|
|
27
|
-
# Build connection options for Trilogy
|
|
28
|
-
connection_options = {
|
|
29
|
-
host: configuration[:host],
|
|
30
|
-
port: configuration[:port],
|
|
31
|
-
database: configuration[:database],
|
|
32
|
-
username: configuration[:username],
|
|
33
|
-
password: configuration[:password],
|
|
34
|
-
socket: configuration[:socket],
|
|
35
|
-
encoding: configuration[:encoding],
|
|
36
|
-
ssl_mode: configuration[:ssl_mode],
|
|
37
|
-
connect_timeout: configuration[:connect_timeout],
|
|
38
|
-
read_timeout: configuration[:read_timeout],
|
|
39
|
-
write_timeout: configuration[:write_timeout]
|
|
40
|
-
}.compact
|
|
41
|
-
|
|
42
|
-
# Create the Trilogy client connection
|
|
43
|
-
require "trilogy"
|
|
44
|
-
client = Trilogy.new(connection_options)
|
|
45
|
-
|
|
46
|
-
# Return our spatial-enabled adapter
|
|
47
|
-
ConnectionAdapters::TrilogisAdapter.new(
|
|
48
|
-
client,
|
|
49
|
-
logger,
|
|
50
|
-
nil,
|
|
51
|
-
configuration
|
|
52
|
-
)
|
|
53
|
-
rescue Trilogy::Error => e
|
|
54
|
-
raise ActiveRecord::NoDatabaseError if e.message.include?("Unknown database")
|
|
55
|
-
|
|
56
|
-
raise
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
18
|
module ConnectionAdapters
|
|
61
19
|
class TrilogisAdapter < TrilogyAdapter
|
|
62
20
|
ADAPTER_NAME = "Trilogis"
|
|
@@ -75,6 +33,12 @@ module ActiveRecord
|
|
|
75
33
|
geometrycollection
|
|
76
34
|
].freeze
|
|
77
35
|
|
|
36
|
+
def self.spatial_type(type)
|
|
37
|
+
name = type.to_s.downcase.delete("_")
|
|
38
|
+
name = "geometrycollection" if name == "geomcollection"
|
|
39
|
+
name if SPATIAL_COLUMN_TYPES.include?(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
78
42
|
# Default SRID for MySQL
|
|
79
43
|
DEFAULT_SRID = 0
|
|
80
44
|
|
|
@@ -83,63 +47,22 @@ module ActiveRecord
|
|
|
83
47
|
# in longitude-latitude order instead of MySQL's default latitude-longitude
|
|
84
48
|
AXIS_ORDER_LONG_LAT = "'axis-order=long-lat'"
|
|
85
49
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
GEOGRAPHIC_SRIDS = [
|
|
89
|
-
4326, # WGS 84 (GPS)
|
|
90
|
-
4269, # NAD83
|
|
91
|
-
4267, # NAD27
|
|
92
|
-
4258, # ETRS89
|
|
93
|
-
4019 # Unknown datum based upon the GRS 1980 ellipsoid
|
|
94
|
-
].freeze
|
|
95
|
-
|
|
96
|
-
# Class method to check if a type is spatial
|
|
97
|
-
def self.spatial_column_options(type)
|
|
98
|
-
SPATIAL_COLUMN_TYPES.include?(type.to_s.downcase)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def initialize(...)
|
|
102
|
-
super
|
|
103
|
-
|
|
104
|
-
# Override the visitor for spatial support
|
|
105
|
-
@visitor = Arel::Visitors::Trilogis.new(self)
|
|
106
|
-
|
|
107
|
-
# Configure RGeo factory generator for SRID-based factory selection
|
|
108
|
-
configure_rgeo_factory_generator
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def adapter_name
|
|
112
|
-
ADAPTER_NAME
|
|
50
|
+
def self.valid_type?(type)
|
|
51
|
+
!!spatial_type(type) || super
|
|
113
52
|
end
|
|
114
53
|
|
|
115
54
|
def supports_spatial?
|
|
116
|
-
|
|
117
|
-
# MariaDB has different spatial support, so we exclude it for now
|
|
118
|
-
!mariadb? && database_version >= "5.7.6"
|
|
55
|
+
!mariadb? && database_version >= "8.0"
|
|
119
56
|
end
|
|
120
57
|
|
|
121
58
|
def default_srid
|
|
122
59
|
DEFAULT_SRID
|
|
123
60
|
end
|
|
124
61
|
|
|
125
|
-
def spatial_column_options(_table_name)
|
|
126
|
-
# Return empty hash as MySQL stores spatial metadata in information_schema
|
|
127
|
-
{}
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def with_connection
|
|
131
|
-
yield self
|
|
132
|
-
end
|
|
133
|
-
|
|
134
62
|
def schema_creation
|
|
135
63
|
Trilogis::SchemaCreation.new(self)
|
|
136
64
|
end
|
|
137
65
|
|
|
138
|
-
# Override valid_type? to include spatial types
|
|
139
|
-
def valid_type?(type)
|
|
140
|
-
SPATIAL_COLUMN_TYPES.include?(type.to_s) || super
|
|
141
|
-
end
|
|
142
|
-
|
|
143
66
|
def native_database_types
|
|
144
67
|
super.merge(
|
|
145
68
|
geometry: { name: "geometry" },
|
|
@@ -163,26 +86,8 @@ module ActiveRecord
|
|
|
163
86
|
if value.is_a?(RGeo::Feature::Instance)
|
|
164
87
|
srid = value.srid || DEFAULT_SRID
|
|
165
88
|
|
|
166
|
-
# For geographic SRIDs, use axis-order parameter to ensure longitude-latitude order
|
|
167
|
-
# MySQL 8.0 defaults to latitude-longitude for geographic SRS, but GIS tools use long-lat
|
|
168
|
-
# ST_GeomFromWKB DOES support axis-order parameter in MySQL 8.0+
|
|
169
89
|
wkb_hex = RGeo::WKRep::WKBGenerator.new(hex_format: true, little_endian: true).generate(value)
|
|
170
|
-
|
|
171
|
-
"ST_GeomFromWKB(0x#{wkb_hex}, #{srid}, #{AXIS_ORDER_LONG_LAT})"
|
|
172
|
-
else
|
|
173
|
-
# For projected SRIDs (like 3857), no axis-order needed - uses cartesian X,Y
|
|
174
|
-
"ST_GeomFromWKB(0x#{wkb_hex}, #{srid})"
|
|
175
|
-
end
|
|
176
|
-
else
|
|
177
|
-
super
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def type_cast(value, column = nil)
|
|
182
|
-
if column&.type == :geometry && value.is_a?(String)
|
|
183
|
-
# Extract SRID from spatial column if available
|
|
184
|
-
srid = column.respond_to?(:srid) ? column.srid : DEFAULT_SRID
|
|
185
|
-
parse_spatial_value(value, srid)
|
|
90
|
+
"ST_GeomFromWKB(0x#{wkb_hex}, #{srid}, #{AXIS_ORDER_LONG_LAT})"
|
|
186
91
|
else
|
|
187
92
|
super
|
|
188
93
|
end
|
|
@@ -190,52 +95,17 @@ module ActiveRecord
|
|
|
190
95
|
|
|
191
96
|
private
|
|
192
97
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
# This ensures the correct factory (Geographic vs Cartesian) is used based on SRID
|
|
196
|
-
factory_store = RGeo::ActiveRecord::SpatialFactoryStore.instance
|
|
197
|
-
|
|
198
|
-
# Register Geographic spherical factory for each geographic SRID
|
|
199
|
-
# The registry will match based on SRID and return the appropriate factory
|
|
200
|
-
GEOGRAPHIC_SRIDS.each do |srid|
|
|
201
|
-
factory_store.register(
|
|
202
|
-
RGeo::Geographic.spherical_factory(srid: srid),
|
|
203
|
-
srid: srid
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def parse_spatial_value(value, srid = DEFAULT_SRID)
|
|
209
|
-
return nil if value.nil?
|
|
210
|
-
|
|
211
|
-
# Ensure SRID is an integer (defensive programming for external callers)
|
|
212
|
-
srid = srid.to_i
|
|
213
|
-
|
|
214
|
-
# Parse WKB hex string using SpatialFactoryStore for correct factory selection
|
|
215
|
-
if value.is_a?(String) && value.match?(/\A[0-9a-fA-F]+\z/)
|
|
216
|
-
factory = RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
|
|
217
|
-
geo_type: "geometry",
|
|
218
|
-
sql_type: "geometry",
|
|
219
|
-
srid: srid
|
|
220
|
-
)
|
|
221
|
-
RGeo::WKRep::WKBParser.new(factory, support_ewkb: true, default_srid: srid).parse([value].pack("H*"))
|
|
222
|
-
else
|
|
223
|
-
value
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Check if a SRID is geographic (uses latitude-longitude coordinate system)
|
|
228
|
-
def geographic_srid?(srid)
|
|
229
|
-
GEOGRAPHIC_SRIDS.include?(srid)
|
|
98
|
+
def arel_visitor
|
|
99
|
+
Arel::Visitors::Trilogis.new(self)
|
|
230
100
|
end
|
|
231
101
|
|
|
232
102
|
# Override type_map to include spatial types
|
|
233
103
|
def type_map
|
|
234
104
|
@type_map ||= begin
|
|
235
|
-
map = super
|
|
105
|
+
map = Type::TypeMap.new(super)
|
|
236
106
|
|
|
237
107
|
# Add spatial types
|
|
238
|
-
SPATIAL_COLUMN_TYPES.each do |geo_type|
|
|
108
|
+
(SPATIAL_COLUMN_TYPES + ["geomcollection"]).each do |geo_type|
|
|
239
109
|
map.register_type(geo_type) do |sql_type|
|
|
240
110
|
Type::Spatial.new(sql_type)
|
|
241
111
|
end
|
|
@@ -244,14 +114,6 @@ module ActiveRecord
|
|
|
244
114
|
map
|
|
245
115
|
end
|
|
246
116
|
end
|
|
247
|
-
|
|
248
|
-
def translate_exception(exception, message:, sql:, binds:)
|
|
249
|
-
if exception.is_a?(::Trilogy::SSLError)
|
|
250
|
-
return ActiveRecord::ConnectionFailed.new(message, connection_pool: @pool)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
super
|
|
254
|
-
end
|
|
255
117
|
end
|
|
256
118
|
end
|
|
257
119
|
end
|
|
@@ -6,7 +6,6 @@ module ActiveRecord
|
|
|
6
6
|
attr_reader :geo_type, :srid
|
|
7
7
|
|
|
8
8
|
def initialize(sql_type = "geometry")
|
|
9
|
-
@sql_type = sql_type
|
|
10
9
|
@geo_type, @srid = self.class.parse_sql_type(sql_type)
|
|
11
10
|
end
|
|
12
11
|
|
|
@@ -87,32 +86,6 @@ module ActiveRecord
|
|
|
87
86
|
|
|
88
87
|
private
|
|
89
88
|
|
|
90
|
-
def parse_sql_type(sql_type)
|
|
91
|
-
original_sql_type = sql_type.to_s
|
|
92
|
-
sql_type = sql_type.to_s.downcase
|
|
93
|
-
|
|
94
|
-
# Only parse known spatial types
|
|
95
|
-
# For non-spatial types, return them unchanged
|
|
96
|
-
spatial_types = %w[geometry point linestring polygon multipoint multilinestring multipolygon geometrycollection]
|
|
97
|
-
|
|
98
|
-
# Check if it's a spatial type
|
|
99
|
-
base_type = sql_type.split("(").first
|
|
100
|
-
return [original_sql_type, 0] unless spatial_types.include?(base_type)
|
|
101
|
-
|
|
102
|
-
# Extract geometry type and SRID from SQL type
|
|
103
|
-
# Examples: "geometry", "point", "linestring", "geometry(Point,4326)"
|
|
104
|
-
if sql_type =~ /(\w+)(?:\((\w+)(?:,(\d+))?\))?/
|
|
105
|
-
geo_type = Regexp.last_match(1)
|
|
106
|
-
sub_type = Regexp.last_match(2)
|
|
107
|
-
srid = Regexp.last_match(3).to_i
|
|
108
|
-
|
|
109
|
-
geo_type = sub_type.downcase if sub_type
|
|
110
|
-
[geo_type, srid]
|
|
111
|
-
else
|
|
112
|
-
[sql_type, 0]
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
89
|
def parse_string(string)
|
|
117
90
|
return nil if string.blank?
|
|
118
91
|
|
|
@@ -121,12 +94,7 @@ module ActiveRecord
|
|
|
121
94
|
srid = Regexp.last_match(1).to_i
|
|
122
95
|
wkt = Regexp.last_match(2)
|
|
123
96
|
|
|
124
|
-
|
|
125
|
-
geo_factory = RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
|
|
126
|
-
geo_type: @geo_type,
|
|
127
|
-
sql_type: @sql_type,
|
|
128
|
-
srid: srid
|
|
129
|
-
)
|
|
97
|
+
geo_factory = spatial_factory(srid)
|
|
130
98
|
begin
|
|
131
99
|
return RGeo::WKRep::WKTParser.new(geo_factory, support_ewkt: true, default_srid: srid).parse(wkt)
|
|
132
100
|
rescue RGeo::Error::ParseError
|
|
@@ -134,12 +102,11 @@ module ActiveRecord
|
|
|
134
102
|
end
|
|
135
103
|
end
|
|
136
104
|
|
|
137
|
-
#
|
|
138
|
-
if string.match?(
|
|
139
|
-
parse_wkt(string)
|
|
140
|
-
# Try to parse as WKB hex
|
|
141
|
-
elsif string.match?(/^[0-9a-fA-F]+$/)
|
|
105
|
+
# Hex can start with a letter, including MySQL's SRID 4326 prefix (e6100000).
|
|
106
|
+
if string.match?(/\A[0-9a-fA-F]+\z/)
|
|
142
107
|
parse_wkb_hex(string)
|
|
108
|
+
elsif string.match?(/^[A-Z]/i)
|
|
109
|
+
parse_wkt(string)
|
|
143
110
|
end
|
|
144
111
|
end
|
|
145
112
|
|
|
@@ -152,84 +119,27 @@ module ActiveRecord
|
|
|
152
119
|
end
|
|
153
120
|
|
|
154
121
|
def parse_wkb_hex(hex_string)
|
|
155
|
-
return nil if hex_string.
|
|
156
|
-
|
|
157
|
-
binary = convert_hex_to_binary(hex_string)
|
|
158
|
-
parse_binary_with_srid(binary)
|
|
159
|
-
rescue RGeo::Error::ParseError, ArgumentError
|
|
160
|
-
nil
|
|
161
|
-
end
|
|
122
|
+
return nil if hex_string.length.odd?
|
|
162
123
|
|
|
163
|
-
|
|
164
|
-
# MySQL returns WKB as hex string
|
|
165
|
-
[hex_string].pack("H*")
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def parse_binary_with_srid(binary)
|
|
169
|
-
# MySQL internal format: first 4 bytes are SRID (little-endian), then WKB
|
|
170
|
-
if binary.length >= 5
|
|
171
|
-
srid = extract_srid(binary)
|
|
172
|
-
wkb_data = binary[4..]
|
|
173
|
-
parse_wkb_with_factory(wkb_data, srid)
|
|
174
|
-
else
|
|
175
|
-
parse_standard_wkb(binary)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def extract_srid(binary)
|
|
180
|
-
# V = unsigned 32-bit little-endian
|
|
181
|
-
binary[0..3].unpack1("V")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def parse_wkb_with_factory(wkb_data, srid)
|
|
185
|
-
geo_factory = create_factory_for_srid(srid)
|
|
186
|
-
RGeo::WKRep::WKBParser.new(geo_factory, support_ewkb: true, default_srid: srid).parse(wkb_data)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def create_factory_for_srid(srid)
|
|
190
|
-
# Geographic SRIDs (4326, 4269, 4267, 4258, 4019) need spherical factory
|
|
191
|
-
if geographic_srid?(srid)
|
|
192
|
-
RGeo::Geographic.spherical_factory(srid: srid)
|
|
193
|
-
else
|
|
194
|
-
RGeo::Cartesian.preferred_factory(srid: srid)
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def parse_standard_wkb(binary)
|
|
199
|
-
# Fall back to standard WKB parsing with EWKB support
|
|
200
|
-
RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid).parse(binary)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Check if SRID represents a geographic coordinate system
|
|
204
|
-
def geographic_srid?(srid)
|
|
205
|
-
# Common geographic SRIDs that use latitude-longitude
|
|
206
|
-
[4326, 4269, 4267, 4258, 4019].include?(srid)
|
|
124
|
+
parse_wkb_binary([hex_string].pack("H*"))
|
|
207
125
|
end
|
|
208
126
|
|
|
209
127
|
def parse_wkb_binary(binary_string)
|
|
210
128
|
return nil if binary_string.nil? || binary_string.empty?
|
|
211
129
|
|
|
212
|
-
# MySQL
|
|
213
|
-
if binary_string.
|
|
214
|
-
srid = binary_string
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
else
|
|
222
|
-
RGeo::Cartesian.preferred_factory(srid: srid)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Support EWKB format
|
|
226
|
-
RGeo::WKRep::WKBParser.new(geo_factory, support_ewkb: true, default_srid: srid).parse(wkb_data)
|
|
227
|
-
else
|
|
228
|
-
# Fall back to standard WKB parsing with EWKB support
|
|
229
|
-
RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid).parse(binary_string)
|
|
130
|
+
# MySQL binary values contain a four-byte SRID prefix followed by WKB.
|
|
131
|
+
if binary_string.bytesize >= 5
|
|
132
|
+
srid = binary_string.unpack1("V")
|
|
133
|
+
begin
|
|
134
|
+
parser = RGeo::WKRep::WKBParser.new(spatial_factory(srid), support_ewkb: true, default_srid: srid)
|
|
135
|
+
return parser.parse(binary_string.byteslice(4..))
|
|
136
|
+
rescue RGeo::Error::ParseError, ArgumentError
|
|
137
|
+
# Standard WKB has no MySQL prefix; parse the complete input below.
|
|
138
|
+
end
|
|
230
139
|
end
|
|
140
|
+
|
|
141
|
+
RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid).parse(binary_string)
|
|
231
142
|
rescue RGeo::Error::ParseError, ArgumentError
|
|
232
|
-
# Failed to parse, return nil
|
|
233
143
|
nil
|
|
234
144
|
end
|
|
235
145
|
|
|
@@ -240,27 +150,22 @@ module ActiveRecord
|
|
|
240
150
|
normalized_hash = hash.transform_keys(&:to_s)
|
|
241
151
|
return unless normalized_hash["type"] && normalized_hash["coordinates"]
|
|
242
152
|
|
|
243
|
-
RGeo::GeoJSON
|
|
153
|
+
unless defined?(RGeo::GeoJSON)
|
|
154
|
+
raise ArgumentError, 'GeoJSON input requires the rgeo-geojson gem and require "rgeo/geo_json"'
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
RGeo::GeoJSON.decode(normalized_hash, geo_factory: spatial_factory)
|
|
244
158
|
end
|
|
245
159
|
|
|
246
|
-
def spatial_factory
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
geo_type: @geo_type,
|
|
252
|
-
sql_type: @sql_type,
|
|
253
|
-
srid: @srid
|
|
254
|
-
)
|
|
160
|
+
def spatial_factory(srid = @srid)
|
|
161
|
+
store = RGeo::ActiveRecord::SpatialFactoryStore.instance
|
|
162
|
+
attrs = { geo_type: @geo_type, sql_type: @geo_type, srid: srid }
|
|
163
|
+
registered = store.registry.any? do |entry|
|
|
164
|
+
entry.attrs.any? && entry.attrs.all? { |key, value| attrs[key] == value }
|
|
255
165
|
end
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
@spatial_factories[@srid] ||= RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
|
|
260
|
-
geo_type: @geo_type,
|
|
261
|
-
sql_type: @sql_type,
|
|
262
|
-
srid: @srid
|
|
263
|
-
)
|
|
166
|
+
# Preserve the existing geographic defaults without mutating the global registry.
|
|
167
|
+
attrs[:sql_type] = "geography" if !registered && [4326, 4269, 4267, 4258, 4019].include?(srid)
|
|
168
|
+
store.factory(attrs)
|
|
264
169
|
end
|
|
265
170
|
end
|
|
266
171
|
end
|
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_record"
|
|
4
4
|
|
|
5
|
-
# Load ActiveRecord dependencies explicitly to ensure correct loading order
|
|
6
|
-
# This approach works across all Ruby versions and avoids autoload issues
|
|
7
|
-
require_relative "active_record/dependency_loader"
|
|
8
|
-
|
|
9
5
|
require "active_record/connection_adapters/trilogy_adapter"
|
|
10
6
|
require "rgeo"
|
|
11
7
|
require "rgeo/active_record"
|
|
12
8
|
|
|
13
9
|
# Load the adapter
|
|
14
10
|
require_relative "active_record/connection_adapters/trilogis_adapter"
|
|
15
|
-
|
|
16
|
-
# Load railtie if Rails is defined
|
|
17
|
-
require_relative "active_record/connection_adapters/trilogis/railtie" if defined?(Rails)
|
metadata
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-trilogis-adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.1.
|
|
4
|
+
version: 8.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ether Moon
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-11-13 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activerecord
|
|
14
15
|
requirement: !ruby/object:Gem::Requirement
|
|
15
16
|
requirements:
|
|
16
|
-
- - "
|
|
17
|
+
- - ">="
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
+
version: 8.1.4.1
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.2'
|
|
19
23
|
type: :runtime
|
|
20
24
|
prerelease: false
|
|
21
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
26
|
requirements:
|
|
23
|
-
- - "
|
|
27
|
+
- - ">="
|
|
24
28
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
29
|
+
version: 8.1.4.1
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.2'
|
|
26
33
|
- !ruby/object:Gem::Dependency
|
|
27
34
|
name: rgeo
|
|
28
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +58,26 @@ dependencies:
|
|
|
51
58
|
- - "~>"
|
|
52
59
|
- !ruby/object:Gem::Version
|
|
53
60
|
version: '8.1'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: trilogy
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 2.13.3
|
|
68
|
+
- - "<"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3'
|
|
71
|
+
type: :runtime
|
|
72
|
+
prerelease: false
|
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 2.13.3
|
|
78
|
+
- - "<"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3'
|
|
54
81
|
- !ruby/object:Gem::Dependency
|
|
55
82
|
name: minitest
|
|
56
83
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -163,23 +190,9 @@ dependencies:
|
|
|
163
190
|
- - "~>"
|
|
164
191
|
- !ruby/object:Gem::Version
|
|
165
192
|
version: '0.6'
|
|
166
|
-
- !ruby/object:Gem::Dependency
|
|
167
|
-
name: trilogy
|
|
168
|
-
requirement: !ruby/object:Gem::Requirement
|
|
169
|
-
requirements:
|
|
170
|
-
- - "~>"
|
|
171
|
-
- !ruby/object:Gem::Version
|
|
172
|
-
version: '2.9'
|
|
173
|
-
type: :development
|
|
174
|
-
prerelease: false
|
|
175
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
-
requirements:
|
|
177
|
-
- - "~>"
|
|
178
|
-
- !ruby/object:Gem::Version
|
|
179
|
-
version: '2.9'
|
|
180
193
|
description: ActiveRecord connection adapter for MySQL. It extends the Rails built-in
|
|
181
194
|
Trilogy adapter and adds spatial extensions support via RGeo. Compatible with Rails
|
|
182
|
-
8.1
|
|
195
|
+
8.1 native Trilogy adapter. Requires Ruby 3.3+ and Rails 8.1.4.1+.
|
|
183
196
|
email: chipseru@gmail.com
|
|
184
197
|
executables: []
|
|
185
198
|
extensions: []
|
|
@@ -187,7 +200,6 @@ extra_rdoc_files: []
|
|
|
187
200
|
files:
|
|
188
201
|
- LICENSE
|
|
189
202
|
- lib/active_record/connection_adapters/trilogis/arel_tosql.rb
|
|
190
|
-
- lib/active_record/connection_adapters/trilogis/railtie.rb
|
|
191
203
|
- lib/active_record/connection_adapters/trilogis/schema_creation.rb
|
|
192
204
|
- lib/active_record/connection_adapters/trilogis/schema_statements.rb
|
|
193
205
|
- lib/active_record/connection_adapters/trilogis/spatial_column.rb
|
|
@@ -196,7 +208,6 @@ files:
|
|
|
196
208
|
- lib/active_record/connection_adapters/trilogis/spatial_table_definition.rb
|
|
197
209
|
- lib/active_record/connection_adapters/trilogis/version.rb
|
|
198
210
|
- lib/active_record/connection_adapters/trilogis_adapter.rb
|
|
199
|
-
- lib/active_record/dependency_loader.rb
|
|
200
211
|
- lib/active_record/tasks/trilogis_database_tasks.rb
|
|
201
212
|
- lib/active_record/type/spatial.rb
|
|
202
213
|
- lib/activerecord-trilogis-adapter.rb
|
|
@@ -205,6 +216,7 @@ licenses:
|
|
|
205
216
|
- MIT
|
|
206
217
|
metadata:
|
|
207
218
|
rubygems_mfa_required: 'true'
|
|
219
|
+
post_install_message:
|
|
208
220
|
rdoc_options: []
|
|
209
221
|
require_paths:
|
|
210
222
|
- lib
|
|
@@ -212,17 +224,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
212
224
|
requirements:
|
|
213
225
|
- - ">="
|
|
214
226
|
- !ruby/object:Gem::Version
|
|
215
|
-
version: 3.
|
|
227
|
+
version: 3.3.0
|
|
216
228
|
- - "<"
|
|
217
229
|
- !ruby/object:Gem::Version
|
|
218
|
-
version: '
|
|
230
|
+
version: '5.0'
|
|
219
231
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
232
|
requirements:
|
|
221
233
|
- - ">="
|
|
222
234
|
- !ruby/object:Gem::Version
|
|
223
235
|
version: '0'
|
|
224
236
|
requirements: []
|
|
225
|
-
rubygems_version: 3.
|
|
237
|
+
rubygems_version: 3.5.22
|
|
238
|
+
signing_key:
|
|
226
239
|
specification_version: 4
|
|
227
240
|
summary: ActiveRecord adapter for MySQL with spatial extensions, built on Trilogy.
|
|
228
241
|
test_files: []
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveRecord
|
|
4
|
-
module ConnectionAdapters
|
|
5
|
-
module Trilogis
|
|
6
|
-
class Railtie < Rails::Railtie
|
|
7
|
-
initializer "trilogis.initialize" do
|
|
8
|
-
ActiveSupport.on_load(:active_record) do
|
|
9
|
-
require "active_record/connection_adapters/trilogis_adapter"
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Register database tasks for spatial databases
|
|
14
|
-
rake_tasks do
|
|
15
|
-
namespace :db do
|
|
16
|
-
namespace :trilogis do
|
|
17
|
-
desc "Create spatial extensions if needed"
|
|
18
|
-
task setup: :environment do
|
|
19
|
-
ActiveRecord::Base.connection.execute(
|
|
20
|
-
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() LIMIT 1"
|
|
21
|
-
)
|
|
22
|
-
puts "Trilogis adapter is ready. MySQL spatial support enabled."
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# ActiveRecord dependency loader for all Ruby versions
|
|
4
|
-
#
|
|
5
|
-
# This file explicitly pre-loads all required ActiveRecord modules in the correct
|
|
6
|
-
# dependency order. This approach:
|
|
7
|
-
#
|
|
8
|
-
# - Ensures consistent loading behavior across all Ruby versions (3.2+)
|
|
9
|
-
# - Avoids reliance on Ruby's autoload mechanism, which changed in Ruby 3.4
|
|
10
|
-
# - Prevents "uninitialized constant" errors during module inclusion
|
|
11
|
-
# - Works with ActiveRecord 8.0+ internal structure
|
|
12
|
-
#
|
|
13
|
-
# While this was initially created to solve Ruby 3.4 compatibility issues,
|
|
14
|
-
# using explicit requires for all versions provides better stability and consistency.
|
|
15
|
-
|
|
16
|
-
# Layer 0: ConnectionAdapters module setup (defines .resolve method)
|
|
17
|
-
require "active_record/connection_adapters"
|
|
18
|
-
|
|
19
|
-
# Layer 1: Base modules with no dependencies
|
|
20
|
-
require "active_record/connection_adapters/deduplicable"
|
|
21
|
-
|
|
22
|
-
# Layer 2: Abstract adapter modules
|
|
23
|
-
require "active_record/connection_adapters/abstract/quoting"
|
|
24
|
-
require "active_record/connection_adapters/abstract/database_statements"
|
|
25
|
-
require "active_record/connection_adapters/abstract/schema_statements"
|
|
26
|
-
require "active_record/connection_adapters/abstract/database_limits"
|
|
27
|
-
require "active_record/connection_adapters/abstract/query_cache"
|
|
28
|
-
require "active_record/connection_adapters/abstract/savepoints"
|
|
29
|
-
|
|
30
|
-
# Layer 3: Column and schema definitions
|
|
31
|
-
require "active_record/connection_adapters/column"
|
|
32
|
-
require "active_record/connection_adapters/abstract/schema_definitions"
|
|
33
|
-
|
|
34
|
-
# Layer 4: Connection management (needed by ActiveRecord::Base)
|
|
35
|
-
require "active_record/connection_adapters/abstract/connection_handler"
|
|
36
|
-
|
|
37
|
-
# Layer 5: Abstract MySQL adapter (loads MySQL-specific modules)
|
|
38
|
-
require "active_record/connection_adapters/abstract_mysql_adapter"
|