Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for mysql schema with spatial types/indexes #27813

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,18 @@
* Support for spatial data types in database schema for mysql2 adapter

Now it is possible to have:

create_table :some_table do |t|
t.geometry :geometry_field
t.polygon :polygon_field, null: false, index: { type: :spatial }
t.point :point_field, multi: true
t.linestring :linestring_field
end

without switching to sql schema format and raw sql in migrations.

*Vasily Fedoseyev*

* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+.

MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
Expand Down
Expand Up @@ -51,6 +51,14 @@ def arel_visitor # :nodoc:
binary: { name: "blob", limit: 65535 },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
geometry: { name: "geometry" },
point: { name: "point" },
linestring: { name: "linestring" },
polygon: { name: "polygon" },
multi_geometry: { name: "geometrycollection" },
multi_point: { name: "multipoint" },
multi_linestring: { name: "multilinestring" },
multi_polygon: { name: "multipolygon" },
}

INDEX_TYPES = [:fulltext, :spatial]
Expand Down Expand Up @@ -396,7 +404,7 @@ def indexes(table_name, name = nil) #:nodoc:
end

indexes.last.columns << row[:Column_name]
indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] && mysql_index_type != :spatial
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremy can you vet this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mysql returns non-null Sub_part for spatial index, but does not allow to set it, thus previous behaviour resulted in a schema dump that could not be loaded back.

https://dev.mysql.com/doc/refman/5.7/en/create-index.html :

Spatial indexes (created using SPATIAL INDEX) have these characteristics:
...

  • Column prefix lengths are prohibited. The full width of each column is indexed.

end
end

Expand Down Expand Up @@ -684,6 +692,15 @@ def initialize_type_map(m)
m.alias_type %r(year)i, "integer"
m.alias_type %r(bit)i, "binary"

m.register_type %r(^geometry)i, MysqlGeometry.new
m.register_type %r(^point)i, MysqlPoint.new
m.register_type %r(^linestring)i, MysqlLineString.new
m.register_type %r(^polygon)i, MysqlPolygon.new
m.register_type %r(^geometrycollection)i, MysqlGeometryCollection.new
m.register_type %r(^multipoint)i, MysqlMultiPoint.new
m.register_type %r(^multilinestring)i, MysqlMultiLineString.new
m.register_type %r(^multipolygon)i, MysqlMultiPolygon.new

m.register_type(%r(enum)i) do |sql_type|
limit = sql_type[/^enum\((.+)\)/i, 1]
.split(",").map { |enum| enum.strip.length - 2 }.max
Expand Down Expand Up @@ -1013,9 +1030,65 @@ def cast_value(value)
end
end

class MysqlGeometry < ActiveModel::Type::Binary # :nodoc:
def type
:geometry
end
end

class MysqlPoint < MysqlGeometry # :nodoc:
def type
:point
end
end

class MysqlLineString < MysqlGeometry # :nodoc:
def type
:linestring
end
end

class MysqlPolygon < MysqlGeometry # :nodoc:
def type
:polygon
end
end

class MysqlGeometryCollection < ActiveModel::Type::Binary # :nodoc:
def type
:geometry
end
end

class MysqlMultiPoint < MysqlGeometryCollection # :nodoc:
def type
:point
end
end

class MysqlMultiLineString < MysqlGeometryCollection # :nodoc:
def type
:linestring
end
end

class MysqlMultiPolygon < MysqlGeometryCollection # :nodoc:
def type
:polygon
end
end

ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
ActiveRecord::Type.register(:geometry, MysqlGeometry, adapter: :mysql2)
ActiveRecord::Type.register(:point, MysqlPoint, adapter: :mysql2)
ActiveRecord::Type.register(:linestring, MysqlLineString, adapter: :mysql2)
ActiveRecord::Type.register(:polygon, MysqlPolygon, adapter: :mysql2)
ActiveRecord::Type.register(:multi_geometry, MysqlGeometryCollection, adapter: :mysql2)
ActiveRecord::Type.register(:multi_point, MysqlMultiPoint, adapter: :mysql2)
ActiveRecord::Type.register(:multi_linestring, MysqlMultiLineString, adapter: :mysql2)
ActiveRecord::Type.register(:multi_polygon, MysqlMultiPolygon, adapter: :mysql2)
end
end
end
Expand Up @@ -19,6 +19,10 @@ def auto_increment?
def virtual?
/\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
end

def multi?
/^(geometrycollection|multi)/i.match?(sql_type)
end
end
end
end
Expand Down
Expand Up @@ -54,6 +54,26 @@ def unsigned_float(*args, **options)
def unsigned_decimal(*args, **options)
args.each { |name| column(name, :unsigned_decimal, options) }
end

def geometry(*args, multi: false, **options)
type = multi ? :multi_geometry : :geometry
args.each { |name| column(name, type, options) }
end

def point(*args, multi: false, **options)
type = multi ? :multi_point : :point
args.each { |name| column(name, type, options) }
end

def linestring(*args, multi: false, **options)
type = multi ? :multi_linestring : :linestring
args.each { |name| column(name, type, options) }
end

def polygon(*args, multi: false, **options)
type = multi ? :multi_polygon : :polygon
args.each { |name| column(name, type, options) }
end
end

class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
Expand Down
Expand Up @@ -14,6 +14,7 @@ def column_spec_for_primary_key(column)
def prepare_column_options(column)
spec = super
spec[:unsigned] = "true" if column.unsigned?
spec[:multi] = "true" if column.multi?

if supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
Expand Down
47 changes: 47 additions & 0 deletions activerecord/test/cases/adapters/mysql2/spatial_types_test.rb
@@ -0,0 +1,47 @@
require "cases/helper"
require "support/schema_dumping_helper"

class Mysql2SpatialTypesTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false

setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("spatial_types", force: true, options: "ENGINE=MyISAM") do |t|
t.geometry :geometry_field
t.polygon :polygon_field, null: false, index: { type: :spatial }
t.point :point_field
t.linestring :linestring_field

t.geometry :geometry_multi, multi: true
t.polygon :polygon_multi, multi: true
t.point :point_multi, multi: true
t.linestring :linestring_multi, multi: true
end
end

teardown do
@connection.drop_table "spatial_types", if_exists: true
end

test "schema dump includes spatial types" do
schema = dump_table_schema "spatial_types"
assert_match %r{t.geometry\s+"geometry_field"$}, schema
assert_match %r{t.polygon\s+"polygon_field",\s+null: false$}, schema
assert_match %r{t.point\s+"point_field"$}, schema
assert_match %r{t.linestring\s+"linestring_field"$}, schema

assert_match %r{t.geometry\s+"geometry_multi",\s+multi: true$}, schema
assert_match %r{t.polygon\s+"polygon_multi",\s+multi: true$}, schema
assert_match %r{t.point\s+"point_multi",\s+multi: true$}, schema
assert_match %r{t.linestring\s+"linestring_multi",\s+multi: true$}, schema
end

test "schema dump can be restored" do
schema = dump_table_schema "spatial_types"
@connection.drop_table "spatial_types", if_exists: true
silence_stdout { eval schema }
schema2 = dump_table_schema "spatial_types"
assert_equal schema, schema2
end
end
18 changes: 11 additions & 7 deletions activerecord/test/cases/helper.rb
Expand Up @@ -129,21 +129,25 @@ def disable_extension!(extension, connection)
connection.reconnect!
end

def load_schema
# silence verbose schema loading
def silence_stdout
original_stdout = $stdout
$stdout = StringIO.new
yield
ensure
$stdout = original_stdout
end

def load_schema
adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
adapter_specific_schema_file = SCHEMA_ROOT + "/#{adapter_name}_specific_schema.rb"

load SCHEMA_ROOT + "/schema.rb"
silence_stdout do
load SCHEMA_ROOT + "/schema.rb"

if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
end
end
ensure
$stdout = original_stdout
end

load_schema
Expand Down