Skip to content

Instantly share code, notes, and snippets.

@mickey
Created May 1, 2017 21:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mickey/3f9186bbee1eefa98076a891303e5e83 to your computer and use it in GitHub Desktop.
Save mickey/3f9186bbee1eefa98076a891303e5e83 to your computer and use it in GitHub Desktop.
ActiveRecord patches for partial SPATIAL and JSON support in MySQL
require "spec_helper"
describe ActiveRecord::Type::JSON do
class TestJSON < ActiveRecord::Base
end
before do
connection = ActiveRecord::Base.connection
if connection.table_exists?(:test_jsons)
connection.drop_table(:test_jsons)
end
connection.create_table(:test_jsons) do |t|
t.json :data
end
end
let(:json_data) { {'foo' => 'bar', 'baz' => [1,2,3,'yolo']} }
it "saves, get and updates a json field", truncation: true do
json = TestJSON.create!({
data: json_data
})
expect(json.reload.data).to eq(json_data)
json = TestJSON.new({
data: json_data
})
json.save!
expect(json.reload.data).to eq(json_data)
json.reload
expect(json.reload.data).to eq(json_data)
json.update_attributes!({
data: ['apple', 'banana']
})
expect(json.reload.data).to eq(['apple', 'banana'])
end
end
require "spec_helper"
describe ActiveRecord::Type::MultiPolygon do
class TestMultiPolygon < ActiveRecord::Base
end
before do
connection = ActiveRecord::Base.connection
if connection.table_exists?(:test_multi_polygons)
connection.drop_table(:test_multi_polygons)
end
connection.create_table(:test_multi_polygons) do |t|
t.multipolygon :geom, null: false
end
connection.add_index :test_multi_polygons, :geom, type: :spatial
end
let(:multipolygon_wkt) { 'MULTIPOLYGON(((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))' }
let(:srid) { 4326 }
it "saves multipolygons", truncation: true do
TestMultiPolygon.create!({
geom: { value: multipolygon_wkt, srid: srid}
})
end
it "cannot deserialize multipolygons", truncation: true do
record = TestMultiPolygon.create!({
geom: { value: multipolygon_wkt, srid: srid}
})
expect{ record.reload.geom }.to raise_error(RuntimeError, "Cannot deserialize multipolygons")
end
end
require "spec_helper"
describe ActiveRecord::Type::Point do
class TestPoint < ActiveRecord::Base
end
before do
connection = ActiveRecord::Base.connection
if connection.table_exists?(:test_points)
connection.drop_table(:test_points)
end
connection.create_table(:test_points) do |t|
t.point :coordinates, null: false
end
connection.add_index :test_points, :coordinates, type: :spatial
end
it "saves, get and updates a point", truncation: true do
point = TestPoint.create!({
coordinates: [1.1, 1.2]
})
expect(point.reload.coordinates).to eq([1.1, 1.2])
point = TestPoint.new({
coordinates: [1.3, 1.4]
})
point.save!
expect(point.reload.coordinates).to eq([1.3, 1.4])
point.reload
expect(point.reload.coordinates).to eq([1.3, 1.4])
point.update_attributes!({
coordinates: [3.1, 4.1]
})
expect(point.reload.coordinates).to eq([3.1, 4.1])
end
end
raise "This monkey patch is only guaranteed for Rails 4.2.x" unless Rails.version =~ /4\.2\.\d+/
module ActiveRecord
module Type
class JSON < Value
def binary?
false
end
def type
:json
end
def type_cast_for_database(value)
Oj.dump(value)
end
def type_cast(value)
if value.is_a?(::String)
Oj.load(value)
else
value
end
end
def changed_in_place?(raw_old_value, new_value)
type_cast(raw_old_value) != type_cast(type_cast_for_database(new_value))
end
def type_cast_for_schema(value)
"json"
end
end
end
end
module ActiveRecord::ConnectionAdapters
class TableDefinition
def json(name, options = {})
column(name, :json, options)
end
end
class AbstractMysqlAdapter
def initialize_type_map_with_json(m)
initialize_type_map_without_json(m)
m.register_type %r(json)i, ActiveRecord::Type::JSON.new
end
alias_method_chain :initialize_type_map, :json
end
end
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:json] = { name: "json" }
raise "This monkey patch is only guaranteed for Rails 4.2.x" unless Rails.version =~ /4\.2\.\d+/
module ActiveRecord
module Type
class MultiPolygon < Value
def binary?
true
end
def type
:multipolygon
end
def type_cast_for_database(value)
value
end
def type_cast(value)
raise RuntimeError, "Cannot deserialize multipolygons" if value.is_a?(::String)
value
end
def type_cast_for_schema(value)
"multipolygon"
end
end
end
end
module ActiveRecord::ConnectionAdapters
class TableDefinition
def multipolygon(name, options = {})
column(name, :multipolygon, options)
end
end
class AbstractMysqlAdapter
def quote_with_multipolygon(value, column = nil)
if column && column.cast_type.is_a?(ActiveRecord::Type::MultiPolygon) && value.is_a?(Hash)
"ST_MultiPolygonFromText('#{value[:value]}', #{value.fetch(:srid, 4326).to_i})"
else
quote_without_multipolygon(value, column)
end
end
alias_method_chain :quote, :multipolygon
def initialize_type_map_with_multipolygon(m)
initialize_type_map_without_multipolygon(m)
m.register_type %r(multipolygon)i, ActiveRecord::Type::MultiPolygon.new
end
alias_method_chain :initialize_type_map, :multipolygon
end
end
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:multipolygon] = { name: "multipolygon" }
raise "This monkey patch is only guaranteed for Rails 4.2.x" unless Rails.version =~ /4\.2\.\d+/
module ActiveRecord
module Type
class Point < Value
def binary?
true
end
def type
:point
end
def type_cast_for_database(value)
value
end
def type_cast(value)
if value.is_a?(Array)
value
else
value.unpack('LcLd2')[-2..-1]
end
end
def type_cast_for_schema(value)
"point"
end
end
end
end
module ActiveRecord::ConnectionAdapters
class TableDefinition
def point(name, options = {})
column(name, :point, options)
end
end
class AbstractMysqlAdapter
def quote_with_point(value, column = nil)
if column && column.cast_type.is_a?(ActiveRecord::Type::Point) && value.is_a?(Array)
"POINT(#{value[0]}, #{value[1]})"
else
quote_without_point(value, column)
end
end
alias_method_chain :quote, :point
# Column prefix lengths are prohibited for SPATIAL indexes. The full width of each column is indexed.
# http://dev.mysql.com/doc/refman/5.7/en/create-index.html
def indexes_with_geometry(table_name, name = nil)
indexes = indexes_without_geometry(table_name, name)
indexes.each do |index|
index.lengths = [] if index.type == :spatial && index.lengths.present?
end
indexes
end
alias_method_chain :indexes, :geometry
def initialize_type_map_with_point(m)
initialize_type_map_without_point(m)
m.register_type %r(point)i, ActiveRecord::Type::Point.new
end
alias_method_chain :initialize_type_map, :point
end
end
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:point] = { name: "point" }
module Arel
module Predications
def json_contains(value)
Nodes::NamedFunction.new('JSON_CONTAINS', [self, Nodes.build_quoted(value.to_json)])
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment