activerecord-mysql2rgeo-adapter 7.2.0 → 7.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3cea6a82ae6d953228236a5977edc5959e9c42f1bfb22551e65c9750a68b09b
4
- data.tar.gz: ec5fe7209c439daf1a6c6c0acad0ce456ea88c92e811f17d777a5f6f927e8ad7
3
+ metadata.gz: 22e12f955f45c958cdf1d7482072dea06de6c23a55df0863d0e454cb3b2b4814
4
+ data.tar.gz: 594de2ab90c4ec8cc2ea126ad286eddbf7c44b3ff46a08b204ff92add1baefde
5
5
  SHA512:
6
- metadata.gz: b8525a4fbcb8ca8bf509c1b079aed654f10785403df3eab43dc7ef0f93d5dd1742794da811915b6415bd35b7d7c58dc4d9addcfdeac4a2a54f1581e91c8417d4
7
- data.tar.gz: 392f931518541a7a0f827749393f1cf14064bd31b50ccebe9f236d251035089102b929a8d9bf104dfe1fb08b31f46d229806d857dcd96daa8f32bad0485cc214
6
+ metadata.gz: 5f41a95d660e6c21be4a2b3d653e0d47758d74432a36f1ac68075f2a2b079ec4d0da35e30fd83e6e2d7484abd8d9e8a664752aa397d0024690ae01b4afd84047
7
+ data.tar.gz: 2ff34f00366a8e5861ba4394f1b42d827f1b502e258defe09bd156b28eb9e13f53b7e4b617d8c283fcbe09909885f3002b8a04f8d101723d8c1b7c206975505a
@@ -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.2.0"
6
+ VERSION = "7.3.1"
7
7
  end
8
8
  end
9
9
  end
@@ -47,27 +47,37 @@ module ActiveRecord
47
47
  module ConnectionAdapters
48
48
  class Mysql2RgeoAdapter < Mysql2Adapter
49
49
  ADAPTER_NAME = "Mysql2Rgeo"
50
+ MINIMUM_SUPPORTED_VERSION = "8.0.0"
50
51
 
51
52
  include Mysql2Rgeo::SchemaStatements
52
53
 
53
54
  SPATIAL_COLUMN_OPTIONS =
54
55
  {
56
+ geography: { type: "geometry", geographic: true },
55
57
  geometry: {},
56
58
  geometrycollection: {},
59
+ geometry_collection: { type: "geometrycollection" },
57
60
  linestring: {},
61
+ line_string: { type: "linestring" },
58
62
  multilinestring: {},
63
+ multi_line_string: { type: "multilinestring" },
59
64
  multipoint: {},
65
+ multi_point: { type: "multipoint" },
60
66
  multipolygon: {},
67
+ multi_polygon: { type: "multipolygon" },
61
68
  spatial: { type: "geometry" },
62
69
  point: {},
63
- polygon: {}
70
+ polygon: {},
71
+ st_point: { type: "point" },
72
+ st_polygon: { type: "polygon" }
64
73
  }.freeze
65
74
 
66
- # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
75
+ # MySQL uses SRID 0 when a spatial column does not declare one explicitly.
67
76
  DEFAULT_SRID = 0
68
77
 
69
- def initialize(...)
78
+ def initialize(connection, logger, connection_options, config)
70
79
  super
80
+ verify_supported_database_version!
71
81
 
72
82
  @visitor = Arel::Visitors::Mysql2Rgeo.new(self)
73
83
  end
@@ -86,7 +96,10 @@ module ActiveRecord
86
96
  super.merge(
87
97
  geometry: { name: "geometry" },
88
98
  geometrycollection: { name: "geometrycollection" },
99
+ line_string: { name: "linestring" },
89
100
  linestring: { name: "linestring" },
101
+ st_point: { name: "point" },
102
+ st_polygon: { name: "polygon" },
90
103
  multi_line_string: { name: "multilinestring" },
91
104
  multi_point: { name: "multipoint" },
92
105
  multi_polygon: { name: "multipolygon" },
@@ -102,20 +115,50 @@ module ActiveRecord
102
115
  def initialize_type_map(m)
103
116
  super
104
117
 
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|
118
+ {
119
+ "geography" => "geometry",
120
+ "geometry" => "geometry",
121
+ "geometry_collection" => "geometrycollection",
122
+ "line_string" => "linestring",
123
+ "multi_line_string" => "multilinestring",
124
+ "multi_point" => "multipoint",
125
+ "multi_polygon" => "multipolygon",
126
+ "st_point" => "point",
127
+ "st_polygon" => "polygon",
128
+ }.each do |registered_type, geo_type|
129
+ m.register_type(registered_type) do |sql_type|
130
+ Type::Spatial.new(sql_type.to_s, geo_type: geo_type)
131
+ end
132
+ end
133
+
134
+ [
135
+ /\Ageometry(?:\(.*\))?\z/i,
136
+ /\Ageography(?:\(.*\))?\z/i,
137
+ /\Apoint(?:\s.*)?\z/i,
138
+ /\Alinestring(?:\s.*)?\z/i,
139
+ /\Apolygon(?:\s.*)?\z/i,
140
+ /\Amultipoint(?:\s.*)?\z/i,
141
+ /\Amultilinestring(?:\s.*)?\z/i,
142
+ /\Amultipolygon(?:\s.*)?\z/i,
143
+ /\Ageometrycollection(?:\s.*)?\z/i,
144
+ ].each do |pattern|
145
+ m.register_type(pattern) do |sql_type|
116
146
  Type::Spatial.new(sql_type.to_s)
117
147
  end
118
148
  end
149
+
150
+ {
151
+ st_point: "point",
152
+ st_polygon: "polygon",
153
+ line_string: "linestring",
154
+ multi_line_string: "multilinestring",
155
+ multi_point: "multipoint",
156
+ multi_polygon: "multipolygon",
157
+ }.each do |alias_type, geo_type|
158
+ ActiveRecord::Type.register(alias_type) do |_, **kwargs|
159
+ Type::Spatial.new(geo_type, geo_type: geo_type, **kwargs)
160
+ end
161
+ end
119
162
  end
120
163
  end
121
164
 
@@ -125,23 +168,48 @@ module ActiveRecord
125
168
  end
126
169
 
127
170
  def supports_spatial?
128
- !mariadb? && version >= "5.7.6"
171
+ !mariadb? && database_version >= MINIMUM_SUPPORTED_VERSION
172
+ end
173
+
174
+ def adapter_name
175
+ ADAPTER_NAME
129
176
  end
130
177
 
131
178
  def quote(value)
132
179
  dbval = value.try(:value_for_database) || value
133
180
  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})"
181
+ wkt = RGeo::WKRep::WKTGenerator.new(tag_format: :wkt11, emit_ewkt_srid: false).generate(dbval)
182
+ if dbval.srid == 4326
183
+ "ST_GeomFromText(#{super(wkt)}, #{dbval.srid}, 'axis-order=long-lat')"
184
+ else
185
+ "ST_GeomFromText(#{super(wkt)}, #{dbval.srid})"
186
+ end
135
187
  else
136
188
  super
137
189
  end
138
190
  end
139
191
 
140
- def with_connection
141
- yield self
192
+ def quote_default_expression(value, column) # :nodoc:
193
+ return super unless column.respond_to?(:spatial?) && column.spatial?
194
+
195
+ value = lookup_cast_type(column.sql_type).serialize(value)
196
+ hex = RGeo::WKRep::WKBGenerator.new(
197
+ hex_format: true,
198
+ little_endian: true,
199
+ type_format: :wkb11,
200
+ emit_ewkb_srid: false
201
+ ).generate(value).upcase
202
+ "(ST_GeomFromWKB(x'#{hex}', #{value.srid}))"
142
203
  end
143
204
 
144
205
  private
206
+ def verify_supported_database_version!
207
+ return if database_version >= MINIMUM_SUPPORTED_VERSION
208
+
209
+ raise ActiveRecord::ConnectionNotEstablished,
210
+ "#{ADAPTER_NAME} supports MySQL #{MINIMUM_SUPPORTED_VERSION}+ only (detected #{database_version})"
211
+ end
212
+
145
213
  def type_map
146
214
  emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
147
215
  end
@@ -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
@@ -1,5 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record/connection_adapters/mysql2rgeo_adapter"
4
-
5
- ActiveRecord::ConnectionAdapters.register('mysql2rgeo', 'ActiveRecord::ConnectionAdapters::Mysql2RgeoAdapter', 'active_record/connection_adapters/mysql2rgeo_adapter')
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.2.0
4
+ version: 7.3.1
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-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 7.2.0
19
+ version: 7.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 7.2.0
26
+ version: 7.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rgeo-activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,78 +39,79 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 7.0.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: rgeo
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :runtime
47
+ version: '13.0'
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '12.0'
61
+ version: '5.4'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '12.0'
68
+ version: '5.4'
69
69
  - !ruby/object:Gem::Dependency
70
- name: minitest
70
+ name: mocha
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '5.4'
75
+ version: '1.1'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '5.4'
82
+ version: '1.1'
83
83
  - !ruby/object:Gem::Dependency
84
- name: mocha
84
+ name: benchmark-ips
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.1'
89
+ version: '2.12'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.1'
96
+ version: '2.12'
97
97
  - !ruby/object:Gem::Dependency
98
- name: appraisal
98
+ name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '2.0'
103
+ version: '1.50'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '2.0'
110
+ version: '1.50'
111
111
  description: ActiveRecord connection adapter for MySQL. It is based on the stock MySQL
112
112
  adapter, and adds built-in support for the spatial extensions provided by MySQL.
113
- 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.
114
115
  email: stadia@gmail.com
115
116
  executables: []
116
117
  extensions: []
@@ -132,7 +133,10 @@ files:
132
133
  homepage: http://github.com/stadia/activerecord-mysql2rgeo-adapter
133
134
  licenses:
134
135
  - BSD-3-Clause
135
- 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'
136
140
  post_install_message:
137
141
  rdoc_options: []
138
142
  require_paths:
@@ -141,7 +145,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
145
  requirements:
142
146
  - - ">="
143
147
  - !ruby/object:Gem::Version
144
- version: 2.7.0
148
+ version: 3.0.0
145
149
  required_rubygems_version: !ruby/object:Gem::Requirement
146
150
  requirements:
147
151
  - - ">="