activerecord-trilogis-adapter 7.0.1 → 8.0.0
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/LICENSE +21 -0
- data/lib/active_record/connection_adapters/trilogis/arel_tosql.rb +120 -45
- data/lib/active_record/connection_adapters/trilogis/railtie.rb +21 -14
- data/lib/active_record/connection_adapters/trilogis/schema_creation.rb +7 -7
- data/lib/active_record/connection_adapters/trilogis/schema_statements.rb +169 -40
- data/lib/active_record/connection_adapters/trilogis/spatial_column.rb +56 -42
- data/lib/active_record/connection_adapters/trilogis/spatial_column_info.rb +39 -20
- data/lib/active_record/connection_adapters/trilogis/spatial_expressions.rb +78 -4
- data/lib/active_record/connection_adapters/trilogis/spatial_table_definition.rb +79 -22
- data/lib/active_record/connection_adapters/trilogis/version.rb +1 -1
- data/lib/active_record/connection_adapters/trilogis_adapter.rb +219 -111
- data/lib/active_record/dependency_loader.rb +38 -0
- data/lib/active_record/tasks/trilogis_database_tasks.rb +2 -1
- data/lib/active_record/type/spatial.rb +214 -64
- data/lib/activerecord-trilogis-adapter.rb +15 -2
- metadata +111 -36
- data/LICENSE.txt +0 -29
- data/lib/active_record/connection_adapters/trilogis/column_methods.rb +0 -54
- data/lib/active_record/connection_adapters/trilogis/connection.rb +0 -17
- data/lib/active_record/connection_adapters/trilogis/rails/dbconsole.rb +0 -48
|
@@ -1,164 +1,272 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# connection adapter into ActiveRecord.
|
|
5
|
-
|
|
6
|
-
# :stopdoc:
|
|
7
|
-
|
|
3
|
+
require "rgeo"
|
|
8
4
|
require "rgeo/active_record"
|
|
9
|
-
|
|
10
|
-
require "active_record/connection_adapters"
|
|
11
5
|
require "active_record/connection_adapters/trilogy_adapter"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
require "active_record/type/spatial"
|
|
23
|
-
|
|
24
|
-
# :startdoc:
|
|
6
|
+
require_relative "trilogis/version"
|
|
7
|
+
require_relative "trilogis/schema_creation"
|
|
8
|
+
require_relative "trilogis/schema_statements"
|
|
9
|
+
require_relative "trilogis/spatial_column"
|
|
10
|
+
require_relative "trilogis/spatial_column_info"
|
|
11
|
+
require_relative "trilogis/spatial_table_definition"
|
|
12
|
+
require_relative "trilogis/spatial_expressions"
|
|
13
|
+
require_relative "trilogis/arel_tosql"
|
|
14
|
+
require_relative "../type/spatial"
|
|
15
|
+
require_relative "../tasks/trilogis_database_tasks"
|
|
25
16
|
|
|
26
17
|
module ActiveRecord
|
|
27
|
-
module ConnectionHandling
|
|
28
|
-
# Establishes a connection to the database
|
|
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.
|
|
29
21
|
def trilogis_connection(config)
|
|
30
22
|
configuration = config.dup
|
|
31
23
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
configuration[:host],
|
|
38
|
-
configuration[:port],
|
|
39
|
-
configuration[:database],
|
|
40
|
-
configuration[:username],
|
|
41
|
-
configuration[:password],
|
|
42
|
-
configuration[:socket],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
module ConnectionAdapters
|
|
51
61
|
class TrilogisAdapter < TrilogyAdapter
|
|
52
62
|
ADAPTER_NAME = "Trilogis"
|
|
53
|
-
AXIS_ORDER_LONG_LAT = "'axis-order=long-lat'".freeze
|
|
54
63
|
|
|
55
64
|
include Trilogis::SchemaStatements
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
|
|
66
|
+
# MySQL spatial data types
|
|
67
|
+
SPATIAL_COLUMN_TYPES = %w[
|
|
68
|
+
geometry
|
|
69
|
+
point
|
|
70
|
+
linestring
|
|
71
|
+
polygon
|
|
72
|
+
multipoint
|
|
73
|
+
multilinestring
|
|
74
|
+
multipolygon
|
|
75
|
+
geometrycollection
|
|
76
|
+
].freeze
|
|
77
|
+
|
|
78
|
+
# Default SRID for MySQL
|
|
71
79
|
DEFAULT_SRID = 0
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
|
|
81
|
+
# MySQL 8.0+ supports axis-order option for ST_GeomFromText/ST_GeomFromWKB
|
|
82
|
+
# This is critical for geographic coordinate systems to interpret coordinates
|
|
83
|
+
# in longitude-latitude order instead of MySQL's default latitude-longitude
|
|
84
|
+
AXIS_ORDER_LONG_LAT = "'axis-order=long-lat'"
|
|
85
|
+
|
|
86
|
+
# Common geographic SRIDs that use latitude-longitude by default in MySQL 8.0
|
|
87
|
+
# These need axis-order parameter to work with standard GIS longitude-latitude format
|
|
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)
|
|
87
99
|
end
|
|
88
100
|
|
|
89
|
-
def initialize(
|
|
101
|
+
def initialize(...)
|
|
90
102
|
super
|
|
91
103
|
|
|
104
|
+
# Override the visitor for spatial support
|
|
92
105
|
@visitor = Arel::Visitors::Trilogis.new(self)
|
|
106
|
+
|
|
107
|
+
# Register spatial types
|
|
108
|
+
register_spatial_types
|
|
109
|
+
|
|
110
|
+
# Configure RGeo factory generator for SRID-based factory selection
|
|
111
|
+
configure_rgeo_factory_generator
|
|
93
112
|
end
|
|
94
113
|
|
|
95
|
-
def
|
|
96
|
-
|
|
114
|
+
def adapter_name
|
|
115
|
+
ADAPTER_NAME
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def supports_spatial?
|
|
119
|
+
# MySQL 5.7.6+ supports spatial indexes and functions
|
|
120
|
+
# MariaDB has different spatial support, so we exclude it for now
|
|
121
|
+
!mariadb? && database_version >= "5.7.6"
|
|
97
122
|
end
|
|
98
123
|
|
|
99
124
|
def default_srid
|
|
100
125
|
DEFAULT_SRID
|
|
101
126
|
end
|
|
102
127
|
|
|
128
|
+
def spatial_column_options(_table_name)
|
|
129
|
+
# Return empty hash as MySQL stores spatial metadata in information_schema
|
|
130
|
+
{}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def with_connection
|
|
134
|
+
yield self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def schema_creation
|
|
138
|
+
Trilogis::SchemaCreation.new(self)
|
|
139
|
+
end
|
|
140
|
+
|
|
103
141
|
def native_database_types
|
|
104
|
-
# Add spatial types
|
|
105
|
-
# Reference: https://dev.mysql.com/doc/refman/5.6/en/spatial-type-overview.html
|
|
106
142
|
super.merge(
|
|
107
|
-
geometry:
|
|
108
|
-
|
|
109
|
-
linestring:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
geometry: { name: "geometry" },
|
|
144
|
+
point: { name: "point" },
|
|
145
|
+
linestring: { name: "linestring" },
|
|
146
|
+
line_string: { name: "linestring" },
|
|
147
|
+
polygon: { name: "polygon" },
|
|
148
|
+
multipoint: { name: "multipoint" },
|
|
149
|
+
multi_point: { name: "multipoint" },
|
|
150
|
+
multilinestring: { name: "multilinestring" },
|
|
151
|
+
multi_line_string: { name: "multilinestring" },
|
|
152
|
+
multipolygon: { name: "multipolygon" },
|
|
153
|
+
multi_polygon: { name: "multipolygon" },
|
|
154
|
+
geometrycollection: { name: "geometrycollection" },
|
|
155
|
+
geometry_collection: { name: "geometrycollection" }
|
|
116
156
|
)
|
|
117
157
|
end
|
|
118
158
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
super
|
|
159
|
+
# Quote spatial values for SQL
|
|
160
|
+
def quote(value)
|
|
161
|
+
if value.is_a?(RGeo::Feature::Instance)
|
|
162
|
+
srid = value.srid || DEFAULT_SRID
|
|
124
163
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
].each do |geo_type|
|
|
135
|
-
m.register_type(geo_type,Type.lookup(geo_type.to_sym, adapter: :trilogis))
|
|
136
|
-
end
|
|
164
|
+
# For geographic SRIDs, use axis-order parameter to ensure longitude-latitude order
|
|
165
|
+
# MySQL 8.0 defaults to latitude-longitude for geographic SRS, but GIS tools use long-lat
|
|
166
|
+
# ST_GeomFromWKB DOES support axis-order parameter in MySQL 8.0+
|
|
167
|
+
wkb_hex = RGeo::WKRep::WKBGenerator.new(hex_format: true, little_endian: true).generate(value)
|
|
168
|
+
if geographic_srid?(srid)
|
|
169
|
+
"ST_GeomFromWKB(0x#{wkb_hex}, #{srid}, #{AXIS_ORDER_LONG_LAT})"
|
|
170
|
+
else
|
|
171
|
+
# For projected SRIDs (like 3857), no axis-order needed - uses cartesian X,Y
|
|
172
|
+
"ST_GeomFromWKB(0x#{wkb_hex}, #{srid})"
|
|
137
173
|
end
|
|
174
|
+
else
|
|
175
|
+
super
|
|
176
|
+
end
|
|
138
177
|
end
|
|
139
178
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
179
|
+
def type_cast(value, column = nil)
|
|
180
|
+
if column&.type == :geometry && value.is_a?(String)
|
|
181
|
+
# Extract SRID from spatial column if available
|
|
182
|
+
srid = column.respond_to?(:srid) ? column.srid : DEFAULT_SRID
|
|
183
|
+
parse_spatial_value(value, srid)
|
|
184
|
+
else
|
|
185
|
+
super
|
|
186
|
+
end
|
|
143
187
|
end
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def register_spatial_types
|
|
192
|
+
SPATIAL_COLUMN_TYPES.each do |geo_type|
|
|
193
|
+
ActiveRecord::Type.register(
|
|
194
|
+
geo_type.to_sym,
|
|
195
|
+
Type::Spatial.new(geo_type),
|
|
196
|
+
adapter: :trilogis
|
|
197
|
+
)
|
|
198
|
+
end
|
|
147
199
|
end
|
|
148
200
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
201
|
+
def configure_rgeo_factory_generator
|
|
202
|
+
# Register Geographic factories for geographic SRIDs in RGeo::ActiveRecord::SpatialFactoryStore
|
|
203
|
+
# This ensures the correct factory (Geographic vs Cartesian) is used based on SRID
|
|
204
|
+
factory_store = RGeo::ActiveRecord::SpatialFactoryStore.instance
|
|
205
|
+
|
|
206
|
+
# Register Geographic spherical factory for each geographic SRID
|
|
207
|
+
# The registry will match based on SRID and return the appropriate factory
|
|
208
|
+
GEOGRAPHIC_SRIDS.each do |srid|
|
|
209
|
+
factory_store.register(
|
|
210
|
+
RGeo::Geographic.spherical_factory(srid: srid),
|
|
211
|
+
srid: srid
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def parse_spatial_value(value, srid = DEFAULT_SRID)
|
|
217
|
+
return nil if value.nil?
|
|
218
|
+
|
|
219
|
+
# Ensure SRID is an integer (defensive programming for external callers)
|
|
220
|
+
srid = srid.to_i
|
|
221
|
+
|
|
222
|
+
# Parse WKB hex string using SpatialFactoryStore for correct factory selection
|
|
223
|
+
if value.is_a?(String) && value.match?(/\A[0-9a-fA-F]+\z/)
|
|
224
|
+
factory = RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
|
|
225
|
+
geo_type: "geometry",
|
|
226
|
+
sql_type: "geometry",
|
|
227
|
+
srid: srid
|
|
228
|
+
)
|
|
229
|
+
RGeo::WKRep::WKBParser.new(factory, support_ewkb: true, default_srid: srid).parse([value].pack("H*"))
|
|
153
230
|
else
|
|
154
|
-
|
|
231
|
+
value
|
|
155
232
|
end
|
|
156
233
|
end
|
|
157
234
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
235
|
+
# Check if a SRID is geographic (uses latitude-longitude coordinate system)
|
|
236
|
+
def geographic_srid?(srid)
|
|
237
|
+
GEOGRAPHIC_SRIDS.include?(srid)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Override type_map to include spatial types
|
|
241
|
+
def type_map
|
|
242
|
+
@type_map ||= begin
|
|
243
|
+
map = super.dup
|
|
244
|
+
|
|
245
|
+
# Add spatial types
|
|
246
|
+
SPATIAL_COLUMN_TYPES.each do |geo_type|
|
|
247
|
+
map.register_type(geo_type) do |sql_type|
|
|
248
|
+
Type::Spatial.new(sql_type)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
map
|
|
161
253
|
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def translate_exception(exception, message:, sql:, binds:)
|
|
257
|
+
if exception.is_a?(::Trilogy::SSLError)
|
|
258
|
+
return ActiveRecord::ConnectionFailed.new(message, connection_pool: @pool)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
super
|
|
262
|
+
end
|
|
162
263
|
end
|
|
163
264
|
end
|
|
164
265
|
end
|
|
266
|
+
|
|
267
|
+
# Register the adapter with ActiveRecord
|
|
268
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
269
|
+
"trilogis",
|
|
270
|
+
"ActiveRecord::ConnectionAdapters::TrilogisAdapter",
|
|
271
|
+
"active_record/connection_adapters/trilogis_adapter"
|
|
272
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
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"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Register Trilogis adapter to use MySQL database tasks
|
|
4
|
+
# This ensures rake db:create, db:drop, etc. work correctly
|
|
4
5
|
ActiveRecord::Tasks::DatabaseTasks.register_task(
|
|
5
6
|
"trilogis",
|
|
6
7
|
"ActiveRecord::Tasks::MySQLDatabaseTasks"
|