activerecord-trilogis-adapter 7.0.2 → 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.
@@ -1,164 +1,272 @@
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
+ # 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 self.spatial_column_options(key)
96
- SPATIAL_COLUMN_OPTIONS[key]
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: { 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" }
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
- class << self
120
-
121
- private
122
- def initialize_type_map(m)
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
- %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
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
- 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
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
- def supports_spatial?
146
- !mariadb? && version >= "5.7.6"
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 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})"
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
- super
231
+ value
155
232
  end
156
233
  end
157
234
 
158
- private
159
- def type_map
160
- emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
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
- # 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"