activerecord-mysql2rgeo-adapter 7.1.1 → 7.3.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.

Potentially problematic release.


This version of activerecord-mysql2rgeo-adapter might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 914f4389e4a77e63240ab4e1b4940a3bf3482e196af0444070b56b64b876cb4f
4
- data.tar.gz: 444f389675fc3533653aee6ef04f43afb9533f350302ab32fe3e35ce9ef1e066
3
+ metadata.gz: 1c82a0cb0dbcc28e78febc6535b3aced42be547987218719e93343517942dd16
4
+ data.tar.gz: 2fa96c0d9224d4db1a6cf853f5dbbbf625414134481b361cbc0d28d121ff3c1b
5
5
  SHA512:
6
- metadata.gz: 706fc5e585269a98bb855505107d90f1dbf0dc492a8c68a77d2f189c98c8280f3e97f7fd0ba30617648364de309d07af354c2c4fb8a8184a5e1111c8af3e6e45
7
- data.tar.gz: 7861c790453d1e302726f2f94f8721f54aedeb1d69f56d6415b6d9c60cbea7f521350383158f4fe6a3ec8b7c511c28f136b72d10d830c7575fbb3935dbd0f1e1
6
+ metadata.gz: 8d09ca66bf3116806b61d0ff633c7decdbba69bb4a870de87810ba275770deac6e34c0c5065c45a4f1f1f8c0fd920db88c5affed40adec6e23a60d7d88667b92
7
+ data.tar.gz: b7af75ce324fde5f6e5a51e2aa603a7ddf2f15c84ce47c7940f42f098ae5b2222e7eaad00249af892523f39fdcec22bca413840ef2317a8b49cc45f2705bc753
@@ -28,12 +28,10 @@ module Arel # :nodoc:
28
28
 
29
29
  def visit_String(node, collector)
30
30
  node, srid = Mysql2Rgeo.parse_node(node)
31
- collector << if srid == 0
32
- "#{st_func('ST_WKTToSQL')}(#{quote(node)})"
33
- else
34
- "#{st_func('ST_WKTToSQL')}(#{quote(node)}, #{srid})"
35
- end
31
+ collector << spatial_constant_sql(node, srid)
36
32
  end
33
+ alias visit_RGeo_Feature_Instance visit_String
34
+ alias visit_RGeo_Cartesian_BoundingBox visit_String
37
35
 
38
36
  def visit_RGeo_ActiveRecord_SpatialNamedFunction(node, collector)
39
37
  aggregate(st_func(node.name), node, collector)
@@ -43,21 +41,28 @@ module Arel # :nodoc:
43
41
  case node
44
42
  when String
45
43
  node, srid = Mysql2Rgeo.parse_node(node)
46
- collector << if srid == 0
47
- "#{st_func('ST_WKTToSQL')}(#{quote(node)})"
48
- else
49
- "#{st_func('ST_WKTToSQL')}(#{quote(node)}, #{srid})"
50
- end
44
+ collector << spatial_constant_sql(node, srid)
51
45
  when RGeo::Feature::Instance
52
- collector << visit_RGeo_Feature_Instance(node, collector)
46
+ visit_RGeo_Feature_Instance(node, collector)
53
47
  when RGeo::Cartesian::BoundingBox
54
- collector << visit_RGeo_Cartesian_BoundingBox(node, collector)
48
+ visit_RGeo_Cartesian_BoundingBox(node, collector)
55
49
  else
56
50
  visit(node, collector)
57
51
  end
58
52
  end
59
53
 
60
54
  def self.parse_node(node)
55
+ if RGeo::Feature::Instance === node
56
+ wkt = RGeo::WKRep::WKTGenerator.new(tag_format: :wkt11, emit_ewkt_srid: false).generate(node)
57
+ return [wkt, node.srid]
58
+ end
59
+
60
+ if RGeo::Cartesian::BoundingBox === node
61
+ geometry = node.to_geometry
62
+ wkt = RGeo::WKRep::WKTGenerator.new(tag_format: :wkt11, emit_ewkt_srid: false).generate(geometry)
63
+ return [wkt, geometry.srid]
64
+ end
65
+
61
66
  value, srid = nil, 0
62
67
  if node =~ /.*;.*$/i
63
68
  params = Regexp.last_match(0).split(";")
@@ -76,6 +81,18 @@ module Arel # :nodoc:
76
81
  end
77
82
  [value, srid]
78
83
  end
84
+
85
+ private
86
+
87
+ def spatial_constant_sql(node, srid)
88
+ if srid == 0
89
+ "#{st_func('ST_WKTToSQL')}(#{quote(node)})"
90
+ elsif srid == 4326
91
+ "#{st_func('ST_WKTToSQL')}(#{quote(node)}, #{srid}, 'axis-order=long-lat')"
92
+ else
93
+ "#{st_func('ST_WKTToSQL')}(#{quote(node)}, #{srid})"
94
+ end
95
+ end
79
96
  end
80
97
  end
81
98
  end
@@ -14,6 +14,10 @@ module ActiveRecord
14
14
  column(name, :geometry, **options)
15
15
  end
16
16
 
17
+ def geography(name, options = {})
18
+ column(name, :geometry, geographic: true, **options)
19
+ end
20
+
17
21
  def geometry_collection(name, options = {})
18
22
  column(name, :geometrycollection, **options)
19
23
  end
@@ -42,10 +46,12 @@ module ActiveRecord
42
46
  def point(name, options = {})
43
47
  column(name, :point, **options)
44
48
  end
49
+ alias st_point point
45
50
 
46
51
  def polygon(name, options = {})
47
52
  column(name, :polygon, **options)
48
53
  end
54
+ alias st_polygon polygon
49
55
  end
50
56
  end
51
57
 
@@ -5,14 +5,34 @@ module ActiveRecord
5
5
  module Mysql2Rgeo
6
6
  class SchemaCreation < MySQL::SchemaCreation # :nodoc:
7
7
  private
8
+ def visit_IndexDefinition(o, create = false)
9
+ if o.using&.to_sym == :gist
10
+ sql = create ? ["CREATE SPATIAL INDEX"] : ["SPATIAL INDEX"]
11
+ sql << quote_column_name(o.name)
12
+ sql << "ON #{quote_table_name(o.table)}" if create
13
+ sql << "(#{quoted_columns(o)})"
14
+ return sql.join(" ")
15
+ end
16
+
17
+ super
18
+ end
8
19
 
9
20
  def add_column_options!(sql, options)
10
21
  if options[:srid]
11
22
  sql << " /*!80003 SRID #{options[:srid]} */"
23
+ options = options.except(:srid)
24
+ end
25
+
26
+ if options_include_default?(options) && spatial_column_definition?(options[:column])
27
+ options = options.except(:default)
12
28
  end
13
29
 
14
30
  super
15
31
  end
32
+
33
+ def spatial_column_definition?(column)
34
+ column.respond_to?(:type) && column.type && @conn.class.spatial_column_options(column.type.to_sym)
35
+ end
16
36
  end
17
37
  end
18
38
  end
@@ -13,12 +13,39 @@ module ActiveRecord
13
13
  # https://dev.mysql.com/doc/refman/5.6/en/create-index.html
14
14
  indexes.select do |idx|
15
15
  idx.type == :spatial
16
- end.each { |idx| idx.is_a?(Struct) ? idx.lengths = {} : idx.instance_variable_set(:@lengths, {}) }
16
+ end.each do |idx|
17
+ if idx.is_a?(Struct)
18
+ idx.lengths = {}
19
+ idx.using = :gist if idx.members.include?(:using)
20
+ else
21
+ idx.instance_variable_set(:@lengths, {})
22
+ idx.instance_variable_set(:@using, :gist)
23
+ end
24
+ end
17
25
  indexes
18
26
  end
19
27
 
28
+ def add_index(table_name, column_name, **options) # :nodoc:
29
+ if options[:using]&.to_sym == :gist
30
+ Array(column_name).each do |name|
31
+ column = columns(table_name).find { |col| col.name == name.to_s }
32
+ next unless column&.spatial? && column.null
33
+
34
+ column_options = column.limit.is_a?(Hash) ? column.limit.symbolize_keys.except(:type) : {}
35
+ column_options[:comment] = column.comment if column.comment.present?
36
+ change_column(table_name, name, column.limit[:type].to_sym, **column_options.merge(null: false))
37
+ end
38
+ end
39
+
40
+ super
41
+ end
42
+
20
43
  # override
21
44
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc:
45
+ if type.to_sym == :geometry && limit.is_a?(String)
46
+ return "geometry(#{limit})"
47
+ end
48
+
22
49
  if (info = RGeo::ActiveRecord.geometric_type_from_name(type.to_s.delete("_")))
23
50
  type = limit[:type] || type if limit.is_a?(::Hash)
24
51
  type = type.to_s.delete("_").upcase
@@ -38,20 +65,67 @@ module ActiveRecord
38
65
  Mysql2Rgeo::TableDefinition.new(self, *args, **options)
39
66
  end
40
67
 
68
+ def update_table_definition(table_name, base)
69
+ Mysql2Rgeo::Table.new(table_name, base)
70
+ end
71
+
41
72
  # override
42
73
  def new_column_from_field(table_name, field, _definitions)
43
74
  type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
44
75
  default, default_function = field[:Default], nil
76
+ metadata = Mysql2Rgeo::ColumnDefinitionUtils.extract_metadata(field[:Comment])
77
+ comment = Mysql2Rgeo::ColumnDefinitionUtils.strip_metadata_comment(field[:Comment])
78
+ default = metadata[:default_hex] if default.nil? && metadata[:default_hex].present?
45
79
 
46
80
  if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
47
81
  default, default_function = nil, default
48
82
  elsif type_metadata.extra == "DEFAULT_GENERATED"
49
- default = +"(#{default})" unless default.start_with?("(")
50
- default, default_function = nil, default
83
+ if default == "NULL" && metadata[:default_hex].present?
84
+ default = metadata[:default_hex]
85
+ elsif default == "NULL"
86
+ default = generated_default_for(table_name, field[:Field])
87
+ end
88
+
89
+ if default&.match?(/\Ast_geomfromtext\(/i)
90
+ default = +"(#{default})" unless default.start_with?("(")
91
+ default, default_function = nil, default
92
+ end
93
+ end
94
+
95
+ if type_metadata.extra.to_s.match?(/(?:VIRTUAL|STORED|PERSISTENT)\s+GENERATED/i)
96
+ default_function = generation_expression_for(table_name, field[:Field])
51
97
  end
52
98
 
53
99
  # {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"}
54
100
  spatial = spatial_column_info(table_name).get(field[:Field], type_metadata.sql_type)
101
+ if spatial
102
+ spatial[:has_z] ||= metadata[:has_z]
103
+ spatial[:has_m] ||= metadata[:has_m]
104
+ spatial[:geographic] ||= metadata[:geographic]
105
+ geo_type = spatial[:type].camelize
106
+ geo_type = "#{geo_type}Z" if spatial[:has_z]
107
+ geo_type = "#{geo_type}M" if spatial[:has_m]
108
+ sql_type = if spatial[:geographic]
109
+ "geography(#{geo_type},#{spatial[:srid]})"
110
+ else
111
+ "geometry(#{geo_type},#{spatial[:srid]})"
112
+ end
113
+ type_metadata = MySQL::TypeMetadata.new(
114
+ ConnectionAdapters::SqlTypeMetadata.new(
115
+ sql_type: sql_type,
116
+ type: :geometry,
117
+ limit: nil,
118
+ precision: type_metadata.precision,
119
+ scale: type_metadata.scale,
120
+ ),
121
+ extra: type_metadata.extra,
122
+ )
123
+
124
+ if default_function&.match?(/\A\(?st_geomfromtext\(/i)
125
+ default = extract_spatial_default_hex(default_function, spatial)
126
+ default_function = nil
127
+ end
128
+ end
55
129
 
56
130
  SpatialColumn.new(
57
131
  field[:Field],
@@ -60,8 +134,9 @@ module ActiveRecord
60
134
  field[:Null] == "YES",
61
135
  default_function,
62
136
  collation: field[:Collation],
63
- comment: field[:Comment].presence,
64
- spatial: spatial
137
+ comment: comment,
138
+ spatial: spatial,
139
+ array: metadata[:array]
65
140
  )
66
141
  end
67
142
 
@@ -70,6 +145,58 @@ module ActiveRecord
70
145
  @spatial_column_info ||= {}
71
146
  @spatial_column_info[table_name.to_sym] = SpatialColumnInfo.new(self, table_name.to_s)
72
147
  end
148
+
149
+ def extract_spatial_default_hex(default_function, spatial)
150
+ default_sql = default_function.delete_prefix("(").delete_suffix(")").delete("\\")
151
+ match = default_sql.match(/\Ast_geomfromtext\(_utf8mb4'(.+?)',\s*(\d+)\)\z/i)
152
+ return unless match
153
+
154
+ wkt = match[1]
155
+ srid = match[2].to_i
156
+ type = ActiveRecord::Type::Spatial.new(
157
+ spatial[:geographic] ? "geography" : "geometry",
158
+ geo_type: ActiveRecord::Type::Spatial.normalize_geo_type(spatial[:type]),
159
+ srid: srid,
160
+ geographic: spatial[:geographic],
161
+ has_z: spatial[:has_z],
162
+ has_m: spatial[:has_m],
163
+ )
164
+ geometry = type.serialize(wkt)
165
+ return unless geometry
166
+ geometry = RGeo::Feature.cast(geometry, factory: type.send(:spatial_factory), project: true) if spatial[:geographic]
167
+
168
+ wkb = RGeo::WKRep::WKBGenerator.new(
169
+ hex_format: true,
170
+ little_endian: true,
171
+ type_format: :wkb11,
172
+ emit_ewkb_srid: false
173
+ ).generate(geometry)
174
+ return wkb.upcase unless spatial[:geographic]
175
+
176
+ srid_hex = [spatial[:srid].to_i].pack("V").unpack1("H*")
177
+ "#{srid_hex}#{wkb}".upcase
178
+ end
179
+
180
+ def generation_expression_for(table_name, column_name)
181
+ query_value(<<~SQL)&.gsub("`", "")
182
+ SELECT generation_expression
183
+ FROM information_schema.columns
184
+ WHERE table_schema = DATABASE()
185
+ AND table_name = #{quote(table_name)}
186
+ AND column_name = #{quote(column_name)}
187
+ SQL
188
+ &.gsub(/st_buffer\(([^,]+),\s*(\d+)\)/i, 'st_buffer(\1, (\2)::double precision)')
189
+ end
190
+
191
+ def generated_default_for(table_name, column_name)
192
+ query_value(<<~SQL)
193
+ SELECT column_default
194
+ FROM information_schema.columns
195
+ WHERE table_schema = DATABASE()
196
+ AND table_name = #{quote(table_name)}
197
+ AND column_name = #{quote(column_name)}
198
+ SQL
199
+ end
73
200
  end
74
201
  end
75
202
  end
@@ -4,12 +4,16 @@ module ActiveRecord # :nodoc:
4
4
  module ConnectionAdapters # :nodoc:
5
5
  module Mysql2Rgeo # :nodoc:
6
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, **)
7
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, spatial: nil, array: false, **)
8
8
  @sql_type_metadata = sql_type_metadata
9
+ @array = array
9
10
  if spatial
10
11
  # This case comes from an entry in the geometry_columns table
11
12
  set_geometric_type_from_name(spatial[:type])
12
13
  @srid = spatial[:srid].to_i
14
+ @has_z = spatial[:has_z]
15
+ @has_m = spatial[:has_m]
16
+ @geographic = spatial[:geographic]
13
17
  elsif sql_type =~ /geometry|point|linestring|polygon/i
14
18
  build_from_sql_type(sql_type_metadata.sql_type)
15
19
  elsif sql_type_metadata.sql_type =~ /geometry|point|linestring|polygon/i
@@ -20,23 +24,31 @@ module ActiveRecord # :nodoc:
20
24
  super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
21
25
  if spatial?
22
26
  if @srid
23
- @limit = { type: geometric_type.type_name.underscore, srid: @srid }
27
+ @limit = { type: limit_type_name, srid: @srid }
28
+ @limit[:geographic] = true if geographic?
29
+ @limit[:has_z] = true if has_z?
30
+ @limit[:has_m] = true if has_m?
24
31
  end
25
32
  end
26
33
  end
27
34
 
28
35
  attr_reader :geometric_type, :srid
29
36
 
37
+ def array
38
+ @array || false
39
+ end
40
+ alias array? array
41
+
30
42
  def has_z
31
- false
43
+ spatial? ? (@has_z || false) : nil
32
44
  end
33
45
 
34
46
  def has_m
35
- false
47
+ spatial? ? (@has_m || false) : nil
36
48
  end
37
49
 
38
50
  def geographic
39
- false
51
+ spatial? ? (@geographic || false) : nil
40
52
  end
41
53
 
42
54
  alias geographic? geographic
@@ -59,12 +71,31 @@ module ActiveRecord # :nodoc:
59
71
 
60
72
  def set_geometric_type_from_name(name)
61
73
  @geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
74
+ @geo_type_name = ActiveRecord::Type::Spatial.normalize_geo_type(name)
62
75
  end
63
76
 
64
77
  def build_from_sql_type(sql_type)
65
- geo_type, @srid = Type::Spatial.parse_sql_type(sql_type)
78
+ geo_type, @srid, @has_z, @has_m, @geographic = Type::Spatial.parse_sql_type(sql_type)
66
79
  set_geometric_type_from_name(geo_type)
67
80
  end
81
+
82
+ def limit_type_name
83
+ type_name = @geo_type_name || geometric_type.type_name.underscore
84
+ case type_name
85
+ when "point", "polygon"
86
+ "st_#{type_name}"
87
+ when "linestring"
88
+ "line_string"
89
+ when "multilinestring"
90
+ "multi_line_string"
91
+ when "multipoint"
92
+ "multi_point"
93
+ when "multipolygon"
94
+ "multi_polygon"
95
+ else
96
+ type_name
97
+ end
98
+ end
68
99
  end
69
100
  end
70
101
  end
@@ -13,11 +13,11 @@ module ActiveRecord # :nodoc:
13
13
  def all
14
14
  info = if @adapter.supports_expression_index?
15
15
  @adapter.query(
16
- "SELECT column_name, srs_id, column_type FROM INFORMATION_SCHEMA.Columns WHERE table_name='#{@table_name}'"
16
+ "SELECT column_name, srs_id, column_type, column_comment FROM INFORMATION_SCHEMA.Columns WHERE table_schema = DATABASE() AND table_name='#{@table_name}'"
17
17
  )
18
18
  else
19
19
  @adapter.query(
20
- "SELECT column_name, 0, column_type FROM INFORMATION_SCHEMA.Columns WHERE table_name='#{@table_name}'"
20
+ "SELECT column_name, 0, column_type, column_comment FROM INFORMATION_SCHEMA.Columns WHERE table_schema = DATABASE() AND table_name='#{@table_name}'"
21
21
  )
22
22
  end
23
23
 
@@ -25,11 +25,16 @@ module ActiveRecord # :nodoc:
25
25
  info.each do |row|
26
26
  name = row[0]
27
27
  type = row[2]
28
- type.sub!(/m$/, "")
28
+ column_comment = row[3].to_s
29
+ has_z = type.sub!(/z$/i, "").present?
30
+ has_m = type.sub!(/m$/i, "").present?
29
31
  result[name] = {
30
32
  name: name,
31
33
  srid: row[1].to_i,
32
34
  type: type,
35
+ has_z: has_z,
36
+ has_m: has_m,
37
+ geographic: column_comment.include?("mysql2rgeo:geographic"),
33
38
  }
34
39
  end
35
40
  result
@@ -7,16 +7,43 @@ module ActiveRecord # :nodoc:
7
7
  include ColumnMethods
8
8
  # super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
9
9
  def new_column_definition(name, type, **options)
10
- if (info = Mysql2RgeoAdapter.spatial_column_options(type.to_sym))
10
+ spatial_type = type.to_sym == :virtual ? options[:type]&.to_sym : type.to_sym
11
+
12
+ if spatial_type && (info = Mysql2RgeoAdapter.spatial_column_options(spatial_type))
11
13
  if (limit = options.delete(:limit)) && limit.is_a?(::Hash)
12
14
  options.merge!(limit)
13
15
  end
14
16
 
15
- geo_type = ColumnDefinitionUtils.geo_type(options[:type] || type || info[:type])
17
+ geo_type = if type.to_sym == :virtual
18
+ "GEOMETRY"
19
+ else
20
+ ColumnDefinitionUtils.geo_type(options[:type] || spatial_type || info[:type])
21
+ end
22
+
23
+ unless type.to_sym == :virtual
24
+ options[:srid] ||= ColumnDefinitionUtils.default_srid(options)
25
+ options[:comment] = ColumnDefinitionUtils.add_metadata_comment(
26
+ options[:comment],
27
+ geographic: options[:geographic],
28
+ has_m: options[:has_m],
29
+ has_z: options[:has_z],
30
+ array: options[:array],
31
+ default: options[:default],
32
+ srid: options[:srid],
33
+ geo_type: geo_type
34
+ )
35
+ else
36
+ options.delete(:srid)
37
+ end
16
38
 
17
39
  options[:spatial_type] = geo_type
18
- column = super(name, geo_type.downcase.to_sym, **options)
40
+ if type.to_sym == :virtual
41
+ column = super(name, type, **options.merge(type: geo_type.downcase.to_sym))
42
+ else
43
+ column = super(name, geo_type.downcase.to_sym, **options)
44
+ end
19
45
  else
46
+ options[:comment] = ColumnDefinitionUtils.add_metadata_comment(options[:comment], array: options[:array]) if options[:array]
20
47
  column = super(name, type, **options)
21
48
  end
22
49
 
@@ -24,19 +51,88 @@ module ActiveRecord # :nodoc:
24
51
  end
25
52
 
26
53
  def valid_column_definition_options
27
- super + %i[geographic has_m spatial_type srid]
54
+ super + %i[array geographic has_m has_z spatial_type srid]
28
55
  end
29
56
  end
30
57
 
58
+ class Table < MySQL::Table # :nodoc:
59
+ include ColumnMethods
60
+ end
61
+
31
62
  module ColumnDefinitionUtils
63
+ METADATA_TOKENS = {
64
+ geographic: "mysql2rgeo:geographic",
65
+ has_m: "mysql2rgeo:has_m",
66
+ has_z: "mysql2rgeo:has_z",
67
+ array: "mysql2rgeo:array",
68
+ default_prefix: "mysql2rgeo:default:"
69
+ }.freeze
70
+
32
71
  class << self
33
72
  def geo_type(type = "GEOMETRY")
34
- type.to_s.delete("_").upcase
73
+ type.to_s.sub(/\Ast_/, "").delete("_").upcase
35
74
  end
36
75
 
37
76
  def default_srid(options)
38
77
  options[:geographic] ? 4326 : Mysql2RgeoAdapter::DEFAULT_SRID
39
78
  end
79
+
80
+ def add_metadata_comment(comment, geographic: false, has_m: false, has_z: false, array: false, default: nil, srid: nil, geo_type: nil)
81
+ values = [comment]
82
+ values << METADATA_TOKENS[:geographic] if geographic
83
+ values << METADATA_TOKENS[:has_m] if has_m
84
+ values << METADATA_TOKENS[:has_z] if has_z
85
+ values << METADATA_TOKENS[:array] if array
86
+ if default
87
+ values << "#{METADATA_TOKENS[:default_prefix]}#{encode_default(default, geographic: geographic, srid: srid, geo_type: geo_type, has_m: has_m, has_z: has_z)}"
88
+ end
89
+ values.compact_blank.join(" ")
90
+ end
91
+
92
+ def extract_metadata(comment)
93
+ text = comment.to_s
94
+ default_hex = text[/#{Regexp.escape(METADATA_TOKENS[:default_prefix])}([0-9A-F]+)/i, 1]
95
+ {
96
+ geographic: text.include?(METADATA_TOKENS[:geographic]),
97
+ has_m: text.include?(METADATA_TOKENS[:has_m]),
98
+ has_z: text.include?(METADATA_TOKENS[:has_z]),
99
+ array: text.include?(METADATA_TOKENS[:array]),
100
+ default_hex: default_hex,
101
+ }
102
+ end
103
+
104
+ def strip_metadata_comment(comment)
105
+ text = comment.to_s
106
+ METADATA_TOKENS.each_value do |token|
107
+ text = text.gsub(token, "")
108
+ end
109
+ text = text.gsub(/mysql2rgeo:default:[0-9A-F]+/i, "")
110
+ text.squeeze(" ").strip.presence
111
+ end
112
+
113
+ def encode_default(default, geographic:, srid:, geo_type:, has_m:, has_z:)
114
+ type = ActiveRecord::Type::Spatial.new(
115
+ geographic ? "geography" : "geometry",
116
+ geo_type: ActiveRecord::Type::Spatial.normalize_geo_type(geo_type),
117
+ srid: srid,
118
+ geographic: geographic,
119
+ has_z: has_z,
120
+ has_m: has_m
121
+ )
122
+ geometry = type.serialize(default)
123
+ geometry = RGeo::Feature.cast(geometry, factory: type.send(:spatial_factory), project: true) if geographic
124
+
125
+ wkb = RGeo::WKRep::WKBGenerator.new(
126
+ hex_format: true,
127
+ little_endian: true,
128
+ type_format: :wkb11,
129
+ emit_ewkb_srid: false
130
+ ).generate(geometry).upcase
131
+ return wkb unless geographic
132
+
133
+ srid_hex = [srid.to_i].pack("V").unpack1("H*").upcase
134
+ "#{srid_hex}#{wkb}"
135
+ end
40
136
  end
41
137
  end
42
138
  end
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Mysql2Rgeo
6
- VERSION = "7.1.1"
6
+ VERSION = "7.3.0"
7
7
  end
8
8
  end
9
9
  end
@@ -52,15 +52,23 @@ module ActiveRecord
52
52
 
53
53
  SPATIAL_COLUMN_OPTIONS =
54
54
  {
55
+ geography: { type: "geometry", geographic: true },
55
56
  geometry: {},
56
57
  geometrycollection: {},
58
+ geometry_collection: { type: "geometrycollection" },
57
59
  linestring: {},
60
+ line_string: { type: "linestring" },
58
61
  multilinestring: {},
62
+ multi_line_string: { type: "multilinestring" },
59
63
  multipoint: {},
64
+ multi_point: { type: "multipoint" },
60
65
  multipolygon: {},
66
+ multi_polygon: { type: "multipolygon" },
61
67
  spatial: { type: "geometry" },
62
68
  point: {},
63
- polygon: {}
69
+ polygon: {},
70
+ st_point: { type: "point" },
71
+ st_polygon: { type: "polygon" }
64
72
  }.freeze
65
73
 
66
74
  # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
@@ -86,7 +94,10 @@ module ActiveRecord
86
94
  super.merge(
87
95
  geometry: { name: "geometry" },
88
96
  geometrycollection: { name: "geometrycollection" },
97
+ line_string: { name: "linestring" },
89
98
  linestring: { name: "linestring" },
99
+ st_point: { name: "point" },
100
+ st_polygon: { name: "polygon" },
90
101
  multi_line_string: { name: "multilinestring" },
91
102
  multi_point: { name: "multipoint" },
92
103
  multi_polygon: { name: "multipolygon" },
@@ -102,20 +113,50 @@ module ActiveRecord
102
113
  def initialize_type_map(m)
103
114
  super
104
115
 
105
- %w[
106
- geometry
107
- geometrycollection
108
- point
109
- linestring
110
- polygon
111
- multipoint
112
- multilinestring
113
- multipolygon
114
- ].each do |geo_type|
115
- m.register_type(geo_type) do |sql_type|
116
+ {
117
+ "geography" => "geometry",
118
+ "geometry" => "geometry",
119
+ "geometry_collection" => "geometrycollection",
120
+ "line_string" => "linestring",
121
+ "multi_line_string" => "multilinestring",
122
+ "multi_point" => "multipoint",
123
+ "multi_polygon" => "multipolygon",
124
+ "st_point" => "point",
125
+ "st_polygon" => "polygon",
126
+ }.each do |registered_type, geo_type|
127
+ m.register_type(registered_type) do |sql_type|
128
+ Type::Spatial.new(sql_type.to_s, geo_type: geo_type)
129
+ end
130
+ end
131
+
132
+ [
133
+ /\Ageometry(?:\(.*\))?\z/i,
134
+ /\Ageography(?:\(.*\))?\z/i,
135
+ /\Apoint(?:\s.*)?\z/i,
136
+ /\Alinestring(?:\s.*)?\z/i,
137
+ /\Apolygon(?:\s.*)?\z/i,
138
+ /\Amultipoint(?:\s.*)?\z/i,
139
+ /\Amultilinestring(?:\s.*)?\z/i,
140
+ /\Amultipolygon(?:\s.*)?\z/i,
141
+ /\Ageometrycollection(?:\s.*)?\z/i,
142
+ ].each do |pattern|
143
+ m.register_type(pattern) do |sql_type|
116
144
  Type::Spatial.new(sql_type.to_s)
117
145
  end
118
146
  end
147
+
148
+ {
149
+ st_point: "point",
150
+ st_polygon: "polygon",
151
+ line_string: "linestring",
152
+ multi_line_string: "multilinestring",
153
+ multi_point: "multipoint",
154
+ multi_polygon: "multipolygon",
155
+ }.each do |alias_type, geo_type|
156
+ ActiveRecord::Type.register(alias_type) do |_, **kwargs|
157
+ Type::Spatial.new(geo_type, geo_type: geo_type, **kwargs)
158
+ end
159
+ end
119
160
  end
120
161
  end
121
162
 
@@ -128,15 +169,41 @@ module ActiveRecord
128
169
  !mariadb? && version >= "5.7.6"
129
170
  end
130
171
 
172
+ def postgis_lib_version
173
+ "3.0.mysql2rgeo"
174
+ end
175
+
176
+ def adapter_name
177
+ "PostGIS"
178
+ end
179
+
131
180
  def quote(value)
132
181
  dbval = value.try(:value_for_database) || value
133
182
  if RGeo::Feature::Geometry.check_type(dbval)
134
- "ST_GeomFromWKB(0x#{RGeo::WKRep::WKBGenerator.new(hex_format: true, little_endian: true).generate(dbval)},#{dbval.srid})"
183
+ wkt = RGeo::WKRep::WKTGenerator.new(tag_format: :wkt11, emit_ewkt_srid: false).generate(dbval)
184
+ if dbval.srid == 4326
185
+ "ST_GeomFromText(#{super(wkt)}, #{dbval.srid}, 'axis-order=long-lat')"
186
+ else
187
+ "ST_GeomFromText(#{super(wkt)}, #{dbval.srid})"
188
+ end
135
189
  else
136
190
  super
137
191
  end
138
192
  end
139
193
 
194
+ def quote_default_expression(value, column) # :nodoc:
195
+ return super unless column.respond_to?(:spatial?) && column.spatial?
196
+
197
+ value = lookup_cast_type(column.sql_type).serialize(value)
198
+ hex = RGeo::WKRep::WKBGenerator.new(
199
+ hex_format: true,
200
+ little_endian: true,
201
+ type_format: :wkb11,
202
+ emit_ewkb_srid: false
203
+ ).generate(value).upcase
204
+ "(ST_GeomFromWKB(x'#{hex}', #{value.srid}))"
205
+ end
206
+
140
207
  private
141
208
  def type_map
142
209
  emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
@@ -9,9 +9,14 @@ module ActiveRecord
9
9
  # "geography"
10
10
  # "geometry NOT NULL"
11
11
  # "geometry"
12
- def initialize(sql_type = "geometry")
13
- @sql_type = sql_type
14
- @geo_type, @srid = self.class.parse_sql_type(sql_type)
12
+ def initialize(sql_type = "geometry", geo_type: nil, srid: nil, geographic: false, has_z: false, has_m: false, **_options)
13
+ @sql_type = geographic ? "geography" : sql_type
14
+ parsed_geo_type, parsed_srid, parsed_has_z, parsed_has_m, parsed_geographic = self.class.parse_sql_type(@sql_type)
15
+ @geo_type = self.class.normalize_geo_type(geo_type || parsed_geo_type)
16
+ @srid = srid || parsed_srid
17
+ @has_z = has_z || parsed_has_z
18
+ @has_m = has_m || parsed_has_m
19
+ @geographic = geographic || parsed_geographic
15
20
  end
16
21
 
17
22
  # sql_type: geometry, geometry(Point), geometry(Point,4326), ...
@@ -20,23 +25,51 @@ module ActiveRecord
20
25
  # geo_type: geography, geometry, point, line_string, polygon, ...
21
26
  # srid: 1234
22
27
  def self.parse_sql_type(sql_type)
23
- geo_type, srid = nil, 0, false, false
28
+ geo_type = nil
29
+ srid = 0
30
+ has_z = false
31
+ has_m = false
32
+ geographic = false
33
+
24
34
  if sql_type =~ /(geography|geometry)\((.*)\)$/i
25
35
  # geometry(Point)
26
36
  # geometry(Point,4326)
37
+ geographic = Regexp.last_match(1).casecmp("geography").zero?
27
38
  params = Regexp.last_match(2).split(",")
28
39
  if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
29
40
  geo_type = Regexp.last_match(1)
41
+ has_z = Regexp.last_match(2).casecmp("z").zero?
42
+ has_m = Regexp.last_match(3).casecmp("m").zero?
30
43
  end
31
44
  if params.last =~ /(\d+)/
32
45
  srid = Regexp.last_match(1).to_i
33
46
  end
47
+ elsif sql_type =~ /\A(geography|geometry)\z/i
48
+ geographic = Regexp.last_match(1).casecmp("geography").zero?
49
+ geo_type = Regexp.last_match(1)
34
50
  else
35
51
  # geometry
36
52
  # otherType(a,b)
37
53
  geo_type = sql_type
38
54
  end
39
- [geo_type, srid]
55
+ [geo_type, srid, has_z, has_m, geographic]
56
+ end
57
+
58
+ def self.normalize_geo_type(geo_type)
59
+ case geo_type.to_s.underscore.delete("_")
60
+ when "geometrycollection"
61
+ "geometry_collection"
62
+ when "linestring"
63
+ "line_string"
64
+ when "multilinestring"
65
+ "multi_line_string"
66
+ when "multipoint"
67
+ "multi_point"
68
+ when "multipolygon"
69
+ "multi_polygon"
70
+ else
71
+ geo_type.to_s.underscore.presence
72
+ end
40
73
  end
41
74
 
42
75
  def spatial_factory
@@ -45,7 +78,9 @@ module ActiveRecord
45
78
  @spatial_factories[@srid] ||=
46
79
  RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
47
80
  geo_type: @geo_type,
48
- sql_type: @sql_type,
81
+ has_m: @has_m,
82
+ has_z: @has_z,
83
+ sql_type: @geographic ? "geography" : "geometry",
49
84
  srid: @srid
50
85
  )
51
86
  end
@@ -67,6 +102,9 @@ module ActiveRecord
67
102
  return if value.nil?
68
103
 
69
104
  geo_value = cast_value(value)
105
+ if geo_value && !@geographic && @srid.to_i.zero? && geo_value.srid != @srid
106
+ geo_value = RGeo::Feature.cast(geo_value, factory: spatial_factory, project: true)
107
+ end
70
108
 
71
109
  # TODO: - only valid types should be allowed
72
110
  # e.g. linestring is not valid for point column
@@ -89,12 +127,27 @@ module ActiveRecord
89
127
  if ["\x00", "\x01"].include?(marker)
90
128
  @srid = string[0, 4].unpack1(marker == "\x01" ? "V" : "N")
91
129
  RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid).parse(string[4..-1])
92
- elsif string[0, 10] =~ /[0-9a-fA-F]{8}0[01]/
93
- @srid = string[0, 8].to_i(16)
94
- @srid = [@srid].pack("V").unpack("N").first if string[9, 1] == "1"
95
- RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: srid).parse(string[8..-1])
130
+ elsif string.match?(/\A[0-9a-fA-F]+\z/)
131
+ original_srid = @srid
132
+ parser = RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid)
133
+
134
+ begin
135
+ return parser.parse([string].pack("H*"))
136
+ rescue RGeo::Error::ParseError, RGeo::Error::InvalidGeometry
137
+ @srid = original_srid
138
+ end
139
+
140
+ if string[0, 10] =~ /[0-9a-fA-F]{8}0[01]/
141
+ @srid = string[0, 8].to_i(16)
142
+ @srid = [@srid].pack("V").unpack("N").first if string[9, 1] == "1"
143
+ parser = RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid)
144
+ return parser.parse([string[8..-1]].pack("H*"))
145
+ end
146
+
147
+ parser.parse([string].pack("H*"))
96
148
  else
97
- string, @srid = Arel::Visitors::Mysql2Rgeo.parse_node(string)
149
+ string, srid = Arel::Visitors::Mysql2Rgeo.parse_node(string)
150
+ @srid = srid.zero? ? @srid : srid
98
151
  RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid).parse(string)
99
152
  end
100
153
  rescue RGeo::Error::ParseError, RGeo::Error::InvalidGeometry
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-mysql2rgeo-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.1
4
+ version: 7.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yongdae Hwang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-28 00:00:00.000000000 Z
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -39,92 +39,79 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 7.0.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: rgeo
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: securerandom
42
+ name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: 0.3.0
62
- type: :runtime
47
+ version: '13.0'
48
+ type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: 0.3.0
54
+ version: '13.0'
69
55
  - !ruby/object:Gem::Dependency
70
- name: rake
56
+ name: minitest
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: '12.0'
61
+ version: '5.4'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: '12.0'
68
+ version: '5.4'
83
69
  - !ruby/object:Gem::Dependency
84
- name: minitest
70
+ name: mocha
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - "~>"
88
74
  - !ruby/object:Gem::Version
89
- version: '5.4'
75
+ version: '1.1'
90
76
  type: :development
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - "~>"
95
81
  - !ruby/object:Gem::Version
96
- version: '5.4'
82
+ version: '1.1'
97
83
  - !ruby/object:Gem::Dependency
98
- name: mocha
84
+ name: benchmark-ips
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: '2.1'
89
+ version: '2.12'
104
90
  type: :development
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
- version: '2.1'
96
+ version: '2.12'
111
97
  - !ruby/object:Gem::Dependency
112
- name: appraisal
98
+ name: rubocop
113
99
  requirement: !ruby/object:Gem::Requirement
114
100
  requirements:
115
101
  - - "~>"
116
102
  - !ruby/object:Gem::Version
117
- version: '2.0'
103
+ version: '1.50'
118
104
  type: :development
119
105
  prerelease: false
120
106
  version_requirements: !ruby/object:Gem::Requirement
121
107
  requirements:
122
108
  - - "~>"
123
109
  - !ruby/object:Gem::Version
124
- version: '2.0'
110
+ version: '1.50'
125
111
  description: ActiveRecord connection adapter for MySQL. It is based on the stock MySQL
126
112
  adapter, and adds built-in support for the spatial extensions provided by MySQL.
127
- It uses the RGeo library to represent spatial data in Ruby.
113
+ It uses the RGeo library to represent spatial data in Ruby. This gem is maintained
114
+ for MySQL 8.0 and 8.4.
128
115
  email: stadia@gmail.com
129
116
  executables: []
130
117
  extensions: []
@@ -146,7 +133,10 @@ files:
146
133
  homepage: http://github.com/stadia/activerecord-mysql2rgeo-adapter
147
134
  licenses:
148
135
  - BSD-3-Clause
149
- metadata: {}
136
+ metadata:
137
+ source_code_uri: https://github.com/stadia/activerecord-mysql2rgeo-adapter
138
+ documentation_uri: https://github.com/stadia/activerecord-mysql2rgeo-adapter/blob/main/README.md
139
+ rubygems_mfa_required: 'true'
150
140
  post_install_message:
151
141
  rdoc_options: []
152
142
  require_paths:
@@ -155,14 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
145
  requirements:
156
146
  - - ">="
157
147
  - !ruby/object:Gem::Version
158
- version: 2.7.0
148
+ version: 3.0.0
159
149
  required_rubygems_version: !ruby/object:Gem::Requirement
160
150
  requirements:
161
151
  - - ">="
162
152
  - !ruby/object:Gem::Version
163
153
  version: '0'
164
154
  requirements: []
165
- rubygems_version: 3.1.6
155
+ rubygems_version: 3.3.27
166
156
  signing_key:
167
157
  specification_version: 4
168
158
  summary: ActiveRecord adapter for MySQL, based on RGeo.