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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95237ccbfc9b7709877c7f7adf0d60da083a0b64cf2e0521875c92905c512684
4
- data.tar.gz: 4f729bb255d9aaa5cba1ccfdf82fc832b7df068b1a93d5c35e7aa42c3bed887b
3
+ metadata.gz: 8d6986c4a92cdff8495b93cf837edbc946a9401c1a12a2b639bc7440c974d0d6
4
+ data.tar.gz: b5663960d9aa5514a8c495d6603871c20acbdfdbdd534d78da2c70eb8f4e154d
5
5
  SHA512:
6
- metadata.gz: 35b148fb25fd3e22ce7053aa5cf1e3e2da2bd452b9d38487381ba87cba8b77fd4cd00bfb3d69e0804e69e81a650f30f280f825ac9d0845cc2ef3c9796945bc98
7
- data.tar.gz: da2129c0b41779c82e8ba538ffbec3f7473ee1207d4a2148ac87fbc7a09a03eb2cba8835b4c915a00605bef8cf5c25dabaef441a40f077f3195ccd8ee1d523ad
6
+ metadata.gz: 784de6dc127d3c4f584ed540d37391e1bbb88496ff1e05c9e0229686356b4bd69af33635910b7c81dd12844b649f9db9808458d4251734a4531b7261c2ee99bc
7
+ data.tar.gz: 3e9e4ddea564f8e6843baa1f36104b83f5679b5ecb0ed46e8e0edb5c7f97f942534640f5a35d582dbae3a5c6332c59af828eb7a14b41c21b92f6f2637ae53123
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ether
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,78 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Arel # :nodoc:
4
- module Visitors # :nodoc:
5
- class Trilogis < MySQL # :nodoc:
6
-
3
+ module Arel
4
+ module Visitors
5
+ class Trilogis < Arel::Visitors::MySQL
7
6
  include RGeo::ActiveRecord::SpatialToSql
8
7
 
9
- if ::Arel::Visitors.const_defined?(:BindVisitor)
10
- include ::Arel::Visitors::BindVisitor
11
- end
12
-
13
- FUNC_MAP = {
14
- "st_wkttosql" => "ST_GeomFromText",
15
- "st_wkbtosql" => "ST_GeomFromWKB",
16
- "st_length" => "ST_Length"
8
+ # MySQL spatial function mappings
9
+ SPATIAL_FUNCTIONS = {
10
+ "st_contains" => "ST_Contains",
11
+ "st_crosses" => "ST_Crosses",
12
+ "st_disjoint" => "ST_Disjoint",
13
+ "st_distance" => "ST_Distance",
14
+ "st_equals" => "ST_Equals",
15
+ "st_intersects" => "ST_Intersects",
16
+ "st_overlaps" => "ST_Overlaps",
17
+ "st_touches" => "ST_Touches",
18
+ "st_within" => "ST_Within",
19
+ "st_area" => "ST_Area",
20
+ "st_length" => "ST_Length",
21
+ "st_buffer" => "ST_Buffer",
22
+ "st_centroid" => "ST_Centroid",
23
+ "st_envelope" => "ST_Envelope",
24
+ "st_geomfromtext" => "ST_GeomFromText",
25
+ "st_geomfromwkb" => "ST_GeomFromWKB",
26
+ "st_astext" => "ST_AsText",
27
+ "st_asbinary" => "ST_AsBinary",
28
+ "st_srid" => "ST_SRID"
17
29
  }.freeze
18
30
 
31
+ # Override st_func to use our function mappings
19
32
  def st_func(standard_name)
20
- FUNC_MAP[standard_name.downcase] || standard_name
21
- end
22
-
23
- def visit_String(node, collector)
24
- node, srid = Trilogis.parse_node(node)
25
- collector << wkttosql_statement(node, srid)
26
- end
27
-
28
- def visit_RGeo_ActiveRecord_SpatialNamedFunction(node, collector)
29
- aggregate(st_func(node.name), node, collector)
33
+ SPATIAL_FUNCTIONS[standard_name.downcase] || standard_name
30
34
  end
31
35
 
36
+ # Override visit_in_spatial_context to use our axis-order logic
32
37
  def visit_in_spatial_context(node, collector)
33
38
  case node
34
39
  when String
35
- node, srid = Trilogis.parse_node(node)
36
- collector << wkttosql_statement(node, srid)
40
+ visit_wkt_string(node, collector)
37
41
  when RGeo::Feature::Instance
38
- collector << visit_RGeo_Feature_Instance(node, collector)
42
+ visit_RGeo_Feature_Instance(node, collector)
39
43
  when RGeo::Cartesian::BoundingBox
40
- collector << visit_RGeo_Cartesian_BoundingBox(node, collector)
44
+ geom = node.to_geometry
45
+ visit_RGeo_Feature_Instance(geom, collector)
41
46
  else
42
47
  visit(node, collector)
43
48
  end
44
49
  end
45
50
 
46
- def self.parse_node(node)
47
- value, srid = nil, 0
48
- if node =~ /.*;.*$/i
49
- params = Regexp.last_match(0).split(";")
50
- if params.first =~ /(srid|SRID)=\d*/
51
- srid = params.first.split("=").last.to_i
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)
52
58
  else
53
- value = params.first
59
+ super
54
60
  end
55
- if params.last =~ /(srid|SRID)=\d*/
56
- srid = params.last.split("=").last.to_i
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def visit_RGeo_Feature_Instance(node, collector)
67
+ srid = node.srid || 0
68
+ wkt = node.as_text
69
+
70
+ # MySQL ST_GeomFromText supports axis-order option for geographic SRIDs
71
+ # This ensures longitude-latitude order for SRID 4326 (WGS84)
72
+ if srid == 4326
73
+ collector << "ST_GeomFromText('#{wkt}', #{srid}, #{ActiveRecord::ConnectionAdapters::TrilogisAdapter::AXIS_ORDER_LONG_LAT})"
74
+ else
75
+ collector << "ST_GeomFromText('#{wkt}', #{srid})"
76
+ end
77
+ end
78
+
79
+ def visit_wkt_string(wkt, collector)
80
+ # Extract SRID if present in EWKT format
81
+ if wkt =~ /^SRID=(\d+);(.+)$/i
82
+ srid = Regexp.last_match(1).to_i
83
+ clean_wkt = Regexp.last_match(2)
84
+ # Use axis-order for geographic SRID
85
+ if srid == 4326
86
+ collector << "ST_GeomFromText('#{clean_wkt}', #{srid}, #{ActiveRecord::ConnectionAdapters::TrilogisAdapter::AXIS_ORDER_LONG_LAT})"
57
87
  else
58
- value = params.last
88
+ collector << "ST_GeomFromText('#{clean_wkt}', #{srid})"
59
89
  end
60
90
  else
61
- value = node
91
+ collector << "ST_GeomFromText('#{wkt}', 0)"
62
92
  end
63
- [value, srid]
64
93
  end
65
94
 
66
- private
95
+ # Handle spatial function calls
96
+ def visit_Arel_Nodes_NamedFunction(o, collector)
97
+ name = o.name.downcase
98
+ if SPATIAL_FUNCTIONS.key?(name)
99
+ collector << SPATIAL_FUNCTIONS[name]
100
+ collector << "("
101
+ o.expressions.each_with_index do |arg, i|
102
+ collector << ", " if i.positive?
103
+ # Handle string arguments (WKT/EWKT)
104
+ if arg.is_a?(String)
105
+ visit_wkt_string(arg, collector)
106
+ else
107
+ visit(arg, collector)
108
+ end
109
+ end
110
+ collector << ")"
111
+ else
112
+ super
113
+ end
114
+ end
67
115
 
68
- def wkttosql_statement(node, srid)
69
- func_name = st_func("ST_WKTToSQL")
116
+ # Override literal visiting for spatial values
117
+ def visit_Arel_Nodes_Quoted(o, collector)
118
+ if o.value.is_a?(RGeo::Feature::Instance)
119
+ visit_RGeo_Feature_Instance(o.value, collector)
120
+ else
121
+ super
122
+ end
123
+ end
70
124
 
71
- args = [quote(node)]
72
- args << srid unless srid.zero?
73
- args << ActiveRecord::ConnectionAdapters::TrilogisAdapter::AXIS_ORDER_LONG_LAT
125
+ # Handle RGeo spatial constant nodes from rgeo-activerecord gem
126
+ def visit_RGeo_ActiveRecord_SpatialConstantNode(node, collector)
127
+ value = node.delegate
128
+ if value.is_a?(RGeo::Feature::Instance)
129
+ visit_RGeo_Feature_Instance(value, collector)
130
+ elsif value.is_a?(String)
131
+ # Handle WKT strings
132
+ if value.match?(/^[A-Z]/)
133
+ visit_wkt_string(value, collector)
134
+ else
135
+ # Regular string literal
136
+ collector << quote(value)
137
+ end
138
+ else
139
+ # For numeric or other values
140
+ collector << quote(value)
141
+ end
142
+ end
74
143
 
75
- "#{func_name}(#{args.join(', ')})"
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 << ")"
76
151
  end
77
152
  end
78
153
  end
@@ -1,23 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if defined?(Rails)
4
- require "rails/railtie"
5
-
6
- module ActiveRecord
7
- module ConnectionAdapters
8
- class TrilogisAdapter
9
- class Railtie < ::Rails::Railtie
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trilogis
6
+ class Railtie < Rails::Railtie
7
+ initializer "trilogis.initialize" do
10
8
  ActiveSupport.on_load(:active_record) do
11
- require "active_record/connection_adapters/trilogis/connection"
12
- ActiveRecord::Base.public_send :extend, ActiveRecord::ConnectionAdapters::TrilogisAdapter::Connection
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
13
25
  end
14
26
  end
15
27
  end
16
28
  end
17
29
  end
18
30
  end
19
-
20
- if defined?(Rails::DBConsole)
21
- require "trilogy_adapter/rails/dbconsole"
22
- Rails::DBConsole.prepend(ActiveRecord::ConnectionAdapters::TrilogisAdapter::Rails::DBConsole)
23
- end
@@ -3,16 +3,16 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogis
6
- class SchemaCreation < MySQL::SchemaCreation # :nodoc:
6
+ class SchemaCreation < MySQL::SchemaCreation
7
7
  private
8
8
 
9
- def add_column_options!(sql, options)
10
- if options[:srid]
11
- sql << " /*!80003 SRID #{options[:srid]} */"
12
- end
9
+ def add_column_options!(sql, options)
10
+ # Add SRID option for spatial columns in MySQL 8.0+
11
+ # Format: /*!80003 SRID #{srid} */
12
+ sql << " /*!80003 SRID #{options[:srid]} */" if options[:srid]
13
13
 
14
- super
15
- end
14
+ super
15
+ end
16
16
  end
17
17
  end
18
18
  end
@@ -4,71 +4,200 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogis
6
6
  module SchemaStatements
7
- # super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
7
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
8
+ if spatial_type?(type.to_s)
9
+ # If limit[:type] is specified, use it as the geometry type (e.g., "point")
10
+ # Otherwise use the base type (e.g., "geometry")
11
+ base_type = if limit.is_a?(Hash) && limit[:type]
12
+ limit[:type]
13
+ else
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
22
+ end
8
23
 
9
- # override
10
- def indexes(table_name) #:nodoc:
24
+ def add_index(table_name, column_name, **options)
25
+ index_type = options[:type]
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
+
34
+ super
35
+ end
36
+
37
+ def indexes(table_name)
11
38
  indexes = super
12
- # HACK(aleks, 06/15/18): MySQL 5 does not support prefix lengths for spatial indexes
13
- # https://dev.mysql.com/doc/refman/5.6/en/create-index.html
14
- indexes.select do |idx|
15
- idx.type == :spatial
16
- end.each { |idx| idx.is_a?(Struct) ? idx.lengths = {} : idx.instance_variable_set(:@lengths, {}) }
39
+
40
+ # MySQL doesn't support prefix lengths for spatial indexes
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
45
+ end
46
+
17
47
  indexes
18
48
  end
19
49
 
20
- # override
21
- def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc:
22
- if (info = RGeo::ActiveRecord.geometric_type_from_name(type.to_s.delete("_")))
23
- type = limit[:type] || type if limit.is_a?(::Hash)
24
- type = type.to_s.delete("_").upcase
50
+ # Override columns to use parent's implementation but enhance spatial columns
51
+ # DO NOT override - let parent class handle all column creation
52
+
53
+ # Override to properly handle spatial columns creation
54
+ def new_column_from_field(table_name, field, definitions = nil)
55
+ field_name = extract_field_value(field, :Field, :field)
56
+ sql_type = extract_field_value(field, :Type, :type)
57
+
58
+ if spatial_type?(sql_type)
59
+ build_spatial_column(table_name, field, field_name, sql_type)
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def create_table_definition(name, **)
66
+ Trilogis::TableDefinition.new(self, name, **)
67
+ end
68
+
69
+ def create_table(table_name, **options, &)
70
+ # Clear spatial cache when creating table with force: true
71
+ # This ensures we don't have stale cache from a previously dropped table
72
+ clear_spatial_cache_for(table_name) if options[:force]
73
+ super
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
+ sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{sql_type}"
81
+
82
+ # Add SRID if specified
83
+ sql << " SRID #{options[:srid]}" if options[:srid] && options[:srid] != 0
84
+
85
+ # Add NULL constraint
86
+ sql << " NOT NULL" if options[:null] == false
87
+
88
+ # Add DEFAULT if specified (allow falsy values like 0/false)
89
+ sql << " DEFAULT #{quote_default_expression(options[:default], nil)}" if options.key?(:default)
90
+
91
+ execute sql
92
+
93
+ # Clear memoized spatial column info for this table
94
+ clear_spatial_cache_for(table_name)
95
+ else
96
+ super
25
97
  end
98
+ end
99
+
100
+ def drop_table(table_name, **options)
101
+ # Clear memoized spatial column info for this table before dropping
102
+ clear_spatial_cache_for(table_name)
26
103
  super
27
104
  end
28
105
 
106
+ def rename_table(table_name, new_name)
107
+ # Clear cache for both old and new table names
108
+ clear_spatial_cache_for(table_name)
109
+ clear_spatial_cache_for(new_name)
110
+ super
111
+ end
112
+
113
+ # Clear all spatial column caches (useful for tests)
114
+ def clear_spatial_cache!
115
+ @spatial_column_info = {}
116
+ end
117
+
118
+ def spatial_sql_type(base_type, subtype = nil)
119
+ sql_type = base_type.to_s.delete("_").upcase
120
+ subtype_sql = subtype.to_s
121
+ if subtype_sql.empty?
122
+ sql_type
123
+ else
124
+ "#{sql_type}(#{subtype_sql.delete('_').upcase})"
125
+ end
126
+ end
127
+
29
128
  private
30
129
 
31
- # override
130
+ def spatial_type?(type)
131
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(type.to_s.downcase)
132
+ end
133
+
32
134
  def schema_creation
33
- Trilogis::SchemaCreation.new(self)
135
+ SchemaCreation.new(self)
34
136
  end
35
137
 
36
- # override
37
- def create_table_definition(*args, **options)
38
- Trilogis::TableDefinition.new(self, *args, **options)
138
+ # Memoized spatial column info per table
139
+ def spatial_column_info(table_name)
140
+ @spatial_column_info ||= {}
141
+ @spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
39
142
  end
40
143
 
41
- # override
42
- def new_column_from_field(table_name, field)
43
- type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
44
- default, default_function = field[:Default], nil
144
+ # Clear spatial cache for a specific table
145
+ def clear_spatial_cache_for(table_name)
146
+ @spatial_column_info&.delete(table_name.to_sym)
147
+ @spatial_column_info&.delete(table_name.to_s)
148
+ end
45
149
 
46
- if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
47
- default, default_function = nil, default
48
- elsif type_metadata.extra == "DEFAULT_GENERATED"
49
- default = +"(#{default})" unless default.start_with?("(")
50
- default, default_function = nil, default
150
+ # Extract field value with case-insensitive key lookup
151
+ def extract_field_value(field, *keys)
152
+ keys.each do |key|
153
+ return field[key] if field.key?(key)
154
+ return field[key.to_s] if field.key?(key.to_s)
51
155
  end
156
+ nil
157
+ end
52
158
 
53
- # {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"}
54
- spatial = spatial_column_info(table_name).get(field[:Field], type_metadata.sql_type)
159
+ # Build a spatial column from field metadata
160
+ def build_spatial_column(table_name, field, field_name, sql_type)
161
+ spatial_info = spatial_column_info(table_name).get(field_name, sql_type)
162
+ type_metadata = fetch_type_metadata(sql_type)
55
163
 
56
164
  SpatialColumn.new(
57
- field[:Field],
58
- default,
165
+ field_name,
166
+ extract_field_value(field, :Default, :default),
59
167
  type_metadata,
60
- field[:Null] == "YES",
61
- default_function,
62
- collation: field[:Collation],
63
- comment: field[:Comment].presence,
64
- spatial: spatial
168
+ extract_field_value(field, :Null, :null) == "YES",
169
+ extract_field_value(field, :Extra, :extra),
170
+ collation: extract_field_value(field, :Collation, :collation),
171
+ comment: extract_field_value(field, :Comment, :comment).presence,
172
+ spatial_info: spatial_info
65
173
  )
66
174
  end
175
+ end
67
176
 
68
- # memoize hash of column infos for tables
69
- def spatial_column_info(table_name)
70
- @spatial_column_info ||= {}
71
- @spatial_column_info[table_name.to_sym] = SpatialColumnInfo.new(self, table_name.to_s)
177
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
178
+ private
179
+
180
+ def visit_ColumnDefinition(o)
181
+ if spatial_column?(o)
182
+ sql_type = spatial_sql_type(o.sql_type, o.options[:type])
183
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
184
+
185
+ # Add SRID if specified (MySQL 8.0+ syntax: COLUMN TYPE SRID value)
186
+ column_sql << " SRID #{o.options[:srid]}" if o.options[:srid] && o.options[:srid] != 0
187
+
188
+ column_sql << " NOT NULL" unless o.null
189
+ column_sql << " DEFAULT #{quote_default_expression(o.default, o)}" unless o.default.nil?
190
+ column_sql
191
+ else
192
+ super
193
+ end
194
+ end
195
+
196
+ def spatial_column?(column)
197
+ # Check both with and without underscores
198
+ sql_type = column.sql_type.to_s.downcase
199
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type) ||
200
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type.delete("_"))
72
201
  end
73
202
  end
74
203
  end
@@ -1,62 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord # :nodoc:
4
- module ConnectionAdapters # :nodoc:
5
- module Trilogis # :nodoc:
6
- class SpatialColumn < ConnectionAdapters::MySQL::Column # :nodoc:
7
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, spatial: nil, **)
8
- @sql_type_metadata = sql_type_metadata
9
- if spatial
10
- # This case comes from an entry in the geometry_columns table
11
- set_geometric_type_from_name(spatial[:type])
12
- @srid = spatial[:srid].to_i
13
- elsif sql_type =~ /geometry|point|linestring|polygon/i
14
- build_from_sql_type(sql_type_metadata.sql_type)
15
- elsif sql_type_metadata.sql_type =~ /geometry|point|linestring|polygon/i
16
- # A geometry column with no geometry_columns entry.
17
- # @geometric_type = geo_type_from_sql_type(sql_type)
18
- build_from_sql_type(sql_type_metadata.sql_type)
19
- end
20
- super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
21
- if spatial? && @srid
22
- @limit = {type: geometric_type.type_name.underscore, srid: @srid}
23
- end
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # Add spatial? method to MySQL::Column for compatibility
6
+ module MySQL
7
+ class Column
8
+ def spatial?
9
+ false
24
10
  end
11
+ end
12
+ end
25
13
 
14
+ module Trilogis
15
+ class SpatialColumn < ActiveRecord::ConnectionAdapters::MySQL::Column
26
16
  attr_reader :geometric_type, :srid
27
17
 
28
- def has_z
29
- false
30
- end
18
+ # Map SQL type strings to RGeo type classes
19
+ GEOMETRIC_TYPES = {
20
+ "geometry" => RGeo::Feature::Geometry,
21
+ "point" => RGeo::Feature::Point,
22
+ "linestring" => RGeo::Feature::LineString,
23
+ "polygon" => RGeo::Feature::Polygon,
24
+ "multipoint" => RGeo::Feature::MultiPoint,
25
+ "multilinestring" => RGeo::Feature::MultiLineString,
26
+ "multipolygon" => RGeo::Feature::MultiPolygon,
27
+ "geometrycollection" => RGeo::Feature::GeometryCollection
28
+ }.freeze
31
29
 
32
- def has_m
33
- false
34
- end
30
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil,
31
+ comment: nil, spatial_info: nil, **)
32
+ super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
35
33
 
36
- alias has_z? has_z
37
- alias has_m? has_m
34
+ return unless spatial?
38
35
 
39
- def multi?
40
- /^(geometrycollection|multi)/i.match?(sql_type)
41
- end
42
-
43
- def limit
44
- spatial? ? @limit : super
36
+ if spatial_info
37
+ # Use spatial info from INFORMATION_SCHEMA if available
38
+ geo_type_str = spatial_info[:type].to_s.downcase
39
+ @geometric_type = GEOMETRIC_TYPES[geo_type_str] || RGeo::Feature::Geometry
40
+ @srid = spatial_info[:srid] || 0
41
+ @has_z = spatial_info[:has_z] || false
42
+ @has_m = spatial_info[:has_m] || false
43
+ else
44
+ # Fallback to extracting from SQL type
45
+ type_info = Type::Spatial.new(sql_type_metadata.sql_type)
46
+ geo_type_str = type_info.geo_type.to_s.downcase
47
+ @geometric_type = GEOMETRIC_TYPES[geo_type_str] || RGeo::Feature::Geometry
48
+ @srid = type_info.srid || 0
49
+ @has_z = false
50
+ @has_m = false
51
+ end
45
52
  end
46
53
 
47
54
  def spatial?
48
- %i[geometry].include?(@sql_type_metadata.type)
55
+ TrilogisAdapter::SPATIAL_COLUMN_TYPES.include?(sql_type_metadata.sql_type.downcase)
49
56
  end
50
57
 
51
- private
58
+ def has_z?
59
+ @has_z || false
60
+ end
52
61
 
53
- def set_geometric_type_from_name(name)
54
- @geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
62
+ def has_m?
63
+ @has_m || false
55
64
  end
56
65
 
57
- def build_from_sql_type(sql_type)
58
- geo_type, @srid = Type::Spatial.parse_sql_type(sql_type)
59
- set_geometric_type_from_name(geo_type)
66
+ # Return limit as hash with spatial metadata for schema dumper
67
+ def limit
68
+ return super unless spatial?
69
+
70
+ {
71
+ type: sql_type_metadata.sql_type.downcase,
72
+ srid: @srid
73
+ }
60
74
  end
61
75
  end
62
76
  end