activerecord-trilogis-adapter 7.0.2 → 8.0.1

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.
@@ -1,164 +1,273 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The activerecord-trilogis-adapter gem installs the *trilogis*
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
- require "active_record/connection_adapters/trilogis/version"
13
- require "active_record/connection_adapters/trilogis/column_methods"
14
- require "active_record/connection_adapters/trilogis/schema_creation"
15
- require "active_record/connection_adapters/trilogis/schema_statements"
16
- require "active_record/connection_adapters/trilogis/spatial_table_definition"
17
- require "active_record/connection_adapters/trilogis/spatial_column"
18
- require "active_record/connection_adapters/trilogis/spatial_column_info"
19
- require "active_record/connection_adapters/trilogis/spatial_expressions"
20
- require "active_record/connection_adapters/trilogis/arel_tosql"
21
- require "active_record/tasks/trilogis_database_tasks"
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 # :nodoc:
28
- # Establishes a connection to the database that's used by all Active Record objects.
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
- # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
33
- # matched rather than number of rows updated.
34
- configuration[:found_rows] = true
35
-
36
- options = [
37
- configuration[:host],
38
- configuration[:port],
39
- configuration[:database],
40
- configuration[:username],
41
- configuration[:password],
42
- configuration[:socket],
43
- 0
44
- ]
45
-
46
- ActiveRecord::ConnectionAdapters::TrilogisAdapter.new nil, logger, options, configuration
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
- SPATIAL_COLUMN_OPTIONS =
58
- {
59
- geometry: {},
60
- geometrycollection: {},
61
- linestring: {},
62
- multilinestring: {},
63
- multipoint: {},
64
- multipolygon: {},
65
- spatial: { type: "geometry" },
66
- point: {},
67
- polygon: {}
68
- }.freeze
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
- GEOGRAPHIC_SRID = 4326
73
-
74
- %w[
75
- geometry
76
- geometrycollection
77
- point
78
- linestring
79
- polygon
80
- multipoint
81
- multilinestring
82
- multipolygon
83
- ].each do |geo_type|
84
- ActiveRecord::Type.register(geo_type.to_sym, adapter: :trilogis) do |sql_type|
85
- Type::Spatial.new(sql_type.to_s)
86
- end
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(connection, logger, connection_options, config)
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
+ # Configure RGeo factory generator for SRID-based factory selection
108
+ configure_rgeo_factory_generator
93
109
  end
94
110
 
95
- def self.spatial_column_options(key)
96
- SPATIAL_COLUMN_OPTIONS[key]
111
+ def adapter_name
112
+ ADAPTER_NAME
113
+ end
114
+
115
+ def supports_spatial?
116
+ # MySQL 5.7.6+ supports spatial indexes and functions
117
+ # MariaDB has different spatial support, so we exclude it for now
118
+ !mariadb? && database_version >= "5.7.6"
97
119
  end
98
120
 
99
121
  def default_srid
100
122
  DEFAULT_SRID
101
123
  end
102
124
 
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
+ def schema_creation
135
+ Trilogis::SchemaCreation.new(self)
136
+ end
137
+
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
+
103
143
  def native_database_types
104
- # Add spatial types
105
- # Reference: https://dev.mysql.com/doc/refman/5.6/en/spatial-type-overview.html
106
144
  super.merge(
107
- geometry: { name: "geometry" },
108
- geometrycollection: { name: "geometrycollection" },
109
- linestring: { name: "linestring" },
110
- multi_line_string: { name: "multilinestring" },
111
- multi_point: { name: "multipoint" },
112
- multi_polygon: { name: "multipolygon" },
113
- spatial: { name: "geometry" },
114
- point: { name: "point" },
115
- polygon: { name: "polygon" }
145
+ geometry: { name: "geometry" },
146
+ point: { name: "point" },
147
+ linestring: { name: "linestring" },
148
+ line_string: { name: "linestring" },
149
+ polygon: { name: "polygon" },
150
+ multipoint: { name: "multipoint" },
151
+ multi_point: { name: "multipoint" },
152
+ multilinestring: { name: "multilinestring" },
153
+ multi_line_string: { name: "multilinestring" },
154
+ multipolygon: { name: "multipolygon" },
155
+ multi_polygon: { name: "multipolygon" },
156
+ geometrycollection: { name: "geometrycollection" },
157
+ geometry_collection: { name: "geometrycollection" }
116
158
  )
117
159
  end
118
160
 
119
- class << self
120
-
121
- private
122
- def initialize_type_map(m)
123
- super
161
+ # Quote spatial values for SQL
162
+ def quote(value)
163
+ if value.is_a?(RGeo::Feature::Instance)
164
+ srid = value.srid || DEFAULT_SRID
124
165
 
125
- %w[
126
- geometry
127
- geometrycollection
128
- point
129
- linestring
130
- polygon
131
- multipoint
132
- multilinestring
133
- multipolygon
134
- ].each do |geo_type|
135
- m.register_type(geo_type,Type.lookup(geo_type.to_sym, adapter: :trilogis))
136
- end
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
+ wkb_hex = RGeo::WKRep::WKBGenerator.new(hex_format: true, little_endian: true).generate(value)
170
+ if geographic_srid?(srid)
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})"
137
175
  end
176
+ else
177
+ super
178
+ end
138
179
  end
139
180
 
140
- TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
141
- TYPE_MAP_WITH_BOOLEAN = Type::TypeMap.new(TYPE_MAP).tap do |m|
142
- m.register_type %r(^tinyint\(1\))i, Type::Boolean.new
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)
186
+ else
187
+ super
188
+ end
143
189
  end
144
190
 
145
- def supports_spatial?
146
- !mariadb? && version >= "5.7.6"
191
+ private
192
+
193
+ def configure_rgeo_factory_generator
194
+ # Register Geographic factories for geographic SRIDs in RGeo::ActiveRecord::SpatialFactoryStore
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
147
206
  end
148
207
 
149
- def quote(value)
150
- dbval = value.try(:value_for_database) || value
151
- if RGeo::Feature::Geometry.check_type(dbval)
152
- "ST_GeomFromWKB(0x#{RGeo::WKRep::WKBGenerator.new(hex_format: true, little_endian: true).generate(dbval)},#{dbval.srid}, #{AXIS_ORDER_LONG_LAT})"
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*"))
153
222
  else
154
- super
223
+ value
155
224
  end
156
225
  end
157
226
 
158
- private
159
- def type_map
160
- emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
227
+ # Check if a SRID is geographic (uses latitude-longitude coordinate system)
228
+ def geographic_srid?(srid)
229
+ GEOGRAPHIC_SRIDS.include?(srid)
230
+ end
231
+
232
+ # Override type_map to include spatial types
233
+ def type_map
234
+ @type_map ||= begin
235
+ map = super.dup
236
+
237
+ # Add spatial types
238
+ SPATIAL_COLUMN_TYPES.each do |geo_type|
239
+ map.register_type(geo_type) do |sql_type|
240
+ Type::Spatial.new(sql_type)
241
+ end
242
+ end
243
+
244
+ map
245
+ end
246
+ 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)
161
251
  end
252
+
253
+ super
254
+ end
162
255
  end
163
256
  end
164
257
  end
258
+
259
+ # Register spatial types globally
260
+ ActiveRecord::ConnectionAdapters::TrilogisAdapter::SPATIAL_COLUMN_TYPES.each do |geo_type|
261
+ ActiveRecord::Type.register(
262
+ geo_type.to_sym,
263
+ ActiveRecord::Type::Spatial,
264
+ adapter: :trilogis
265
+ )
266
+ end
267
+
268
+ # Register the adapter with ActiveRecord
269
+ ActiveRecord::ConnectionAdapters.register(
270
+ "trilogis",
271
+ "ActiveRecord::ConnectionAdapters::TrilogisAdapter",
272
+ "active_record/connection_adapters/trilogis_adapter"
273
+ )
@@ -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
- # Use MySQLDatabaseTasks for Trilogy
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"