├── .gitignore ├── Gemfile ├── init.rb ├── lib ├── spatial_adapter │ ├── version.rb │ ├── base │ │ ├── mysql.rb │ │ └── mysql │ │ │ ├── spatial_column.rb │ │ │ └── adapter.rb │ ├── common.rb │ ├── common │ │ ├── table_definition.rb │ │ ├── schema_definitions.rb │ │ ├── raw_geom_info.rb │ │ ├── spatial_column.rb │ │ └── schema_dumper.rb │ ├── railtie.rb │ ├── mysql2.rb │ ├── jdbcmysql.rb │ ├── mysql.rb │ └── postgresql.rb └── spatial_adapter.rb ├── spec ├── postgresql_spec.rb ├── mysql_spec.rb ├── jdbcmysql_spec.rb ├── mysql2_spec.rb ├── models │ └── common.rb ├── shared │ ├── mysql_schema_dumper_spec.rb │ ├── common_model_actions_spec.rb │ ├── mysql_migration_spec.rb │ ├── mysql_models_spec.rb │ └── mysql_connection_adapter_spec.rb ├── db │ ├── jdbcmysql_raw.rb │ ├── mysql2_raw.rb │ ├── mysql_raw.rb │ └── postgis_raw.rb ├── postgresql │ ├── schema_dumper_spec.rb │ ├── connection_adapter_spec.rb │ ├── models_spec.rb │ └── migration_spec.rb └── spec_helper.rb ├── Rakefile ├── rails └── init.rb ├── MIT-LICENSE ├── spatial_adapter.gemspec └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | .rvmrc 2 | Gemfile.lock 3 | pkg/ 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/rails/init" 2 | -------------------------------------------------------------------------------- /lib/spatial_adapter/version.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | VERSION = '1.3.1' 3 | end 4 | -------------------------------------------------------------------------------- /spec/postgresql_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'postgresql/connection_adapter_spec' 3 | require 'postgresql/migration_spec' 4 | require 'postgresql/models_spec' 5 | require 'postgresql/schema_dumper_spec' 6 | -------------------------------------------------------------------------------- /lib/spatial_adapter/base/mysql.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | module Base 3 | module Mysql 4 | end 5 | end 6 | end 7 | 8 | require 'spatial_adapter/base/mysql/adapter' 9 | require 'spatial_adapter/base/mysql/spatial_column' 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | [:mysql, :mysql2, :jdbcmysql, :postgresql].each do |adapter| 7 | desc "Run specs for #{adapter} adapter" 8 | RSpec::Core::RakeTask.new("spec:#{adapter.to_s}") do |t| 9 | t.pattern = "spec/#{adapter}_spec.rb" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | module Common 3 | end 4 | end 5 | 6 | require 'spatial_adapter/common/raw_geom_info' 7 | require 'spatial_adapter/common/spatial_column' 8 | require 'spatial_adapter/common/schema_definitions' 9 | require 'spatial_adapter/common/schema_dumper' 10 | require 'spatial_adapter/common/table_definition' 11 | -------------------------------------------------------------------------------- /lib/spatial_adapter/base/mysql/spatial_column.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter::Base::Mysql 2 | module SpatialColumn 3 | def string_to_geometry(string) 4 | return string unless string.is_a?(String) 5 | begin 6 | GeoRuby::SimpleFeatures::Geometry.from_ewkb(string[4..-1]) 7 | rescue Exception => exception 8 | nil 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common/table_definition.rb: -------------------------------------------------------------------------------- 1 | require 'spatial_adapter' 2 | 3 | class ActiveRecord::ConnectionAdapters::TableDefinition 4 | SpatialAdapter.geometry_data_types.keys.each do |column_name| 5 | define_method(column_name) do |*args| 6 | options = args.extract_options! 7 | args.each do |name| 8 | column(name, column_name, options) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | # Rails initialization (for Rails 2.x) 2 | # 3 | # This will load the adapter for the currently used database configuration, if 4 | # it exists. 5 | 6 | begin 7 | adapter = ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] 8 | require "spatial_adapter/#{adapter}" 9 | rescue LoadError 10 | raise SpatialAdapter::NotCompatibleError.new("spatial_adapter does not currently support the #{adapter} database.") 11 | end 12 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common/schema_definitions.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/abstract_adapter' 2 | 3 | ActiveRecord::ConnectionAdapters::IndexDefinition.class_eval do 4 | attr_accessor :spatial 5 | 6 | alias_method :initialize_without_spatial, :initialize 7 | def initialize(table, name, unique, columns, spatial = false) 8 | initialize_without_spatial(table, name, unique, columns) 9 | @spatial = spatial 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spatial_adapter/railtie.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | class Railtie < Rails::Railtie 3 | initializer "spatial_adapter.load_current_database_adapter" do 4 | adapter = ActiveRecord::Base.configurations[Rails.env]['adapter'] 5 | begin 6 | require "spatial_adapter/#{adapter}" 7 | rescue LoadError 8 | raise SpatialAdapter::NotCompatibleError.new("spatial_adapter does not currently support the #{adapter} database.") 9 | end 10 | end 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /spec/mysql_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared/mysql_connection_adapter_spec' 3 | require 'shared/mysql_migration_spec' 4 | require 'shared/mysql_schema_dumper_spec' 5 | require 'shared/mysql_models_spec' 6 | require 'shared/common_model_actions_spec' 7 | require 'spatial_adapter/mysql' 8 | require 'db/mysql_raw' 9 | require 'models/common' 10 | 11 | describe ActiveRecord::ConnectionAdapters::MysqlAdapter do 12 | it_should_behave_like 'common model actions' 13 | it_should_behave_like 'a modified mysql adapter' 14 | it_should_behave_like 'spatially enabled migrations' 15 | it_should_behave_like 'spatially enabled schema dump' 16 | it_should_behave_like 'spatially enabled models' 17 | end 18 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common/raw_geom_info.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc: 3 | def convert! 4 | self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then 5 | 6 | if dimension == 4 7 | self.with_m = true 8 | self.with_z = true 9 | elsif dimension == 3 10 | if with_m 11 | self.with_z = false 12 | self.with_m = true 13 | else 14 | self.with_z = true 15 | self.with_m = false 16 | end 17 | else 18 | self.with_z = false 19 | self.with_m = false 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/jdbcmysql_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared/mysql_connection_adapter_spec' 3 | require 'shared/mysql_migration_spec' 4 | require 'shared/mysql_schema_dumper_spec' 5 | require 'shared/mysql_models_spec' 6 | require 'shared/common_model_actions_spec' 7 | require 'spatial_adapter/jdbcmysql' 8 | require 'db/jdbcmysql_raw' 9 | require 'models/common' 10 | 11 | describe ActiveRecord::ConnectionAdapters::MysqlAdapter do 12 | it_should_behave_like 'common model actions' 13 | it_should_behave_like 'a modified mysql adapter' do 14 | let(:establish){ jdbcmysql_connection } 15 | end 16 | it_should_behave_like 'spatially enabled migrations' do 17 | let(:establish){ jdbcmysql_connection } 18 | end 19 | it_should_behave_like 'spatially enabled schema dump' do 20 | let(:establish){ jdbcmysql_connection } 21 | end 22 | it_should_behave_like 'spatially enabled models' do 23 | let(:establish){ jdbcmysql_connection } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/mysql2_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared/mysql_connection_adapter_spec' 3 | require 'shared/mysql_migration_spec' 4 | require 'shared/mysql_schema_dumper_spec' 5 | require 'shared/mysql_models_spec' 6 | require 'shared/common_model_actions_spec' 7 | require 'spatial_adapter/mysql2' 8 | require 'db/mysql2_raw' 9 | require 'models/common' 10 | 11 | describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do 12 | it_should_behave_like 'common model actions' 13 | it_should_behave_like 'a modified mysql adapter' do 14 | let(:establish){ mysql2_connection } 15 | let(:column) do 16 | ActiveRecord::ConnectionAdapters::Mysql2Column 17 | end 18 | let(:spatial_column) do 19 | ActiveRecord::ConnectionAdapters::SpatialMysql2Column 20 | end 21 | end 22 | it_should_behave_like 'spatially enabled migrations' do 23 | let(:establish){ mysql2_connection } 24 | end 25 | it_should_behave_like 'spatially enabled schema dump' do 26 | let(:establish){ mysql2_connection } 27 | end 28 | it_should_behave_like 'spatially enabled models' do 29 | let(:establish){ mysql2_connection } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Guilhem Vellut 2 | Copyright (c) 2010 Pete Deffendol 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/spatial_adapter.rb: -------------------------------------------------------------------------------- 1 | # This file should typically not be directly require'd into your project. You 2 | # should require the database-specific adapter you desire, e.g. 3 | # 4 | # require 'spatial_adapter/postgresql' 5 | # 6 | # Why is this file here? 7 | # 8 | # Mostly to keep Rails happy when using config.gem to specify dependencies. 9 | # The Rails init code (rails/init.rb) will then load the adapter that matches 10 | # your database.yml configuration. 11 | 12 | require 'geo_ruby' 13 | require 'active_record' 14 | 15 | include GeoRuby::SimpleFeatures 16 | 17 | module SpatialAdapter 18 | # Translation of geometric data types 19 | def self.geometry_data_types 20 | { 21 | :point => { :name => "POINT" }, 22 | :line_string => { :name => "LINESTRING" }, 23 | :polygon => { :name => "POLYGON" }, 24 | :geometry_collection => { :name => "GEOMETRYCOLLECTION" }, 25 | :multi_point => { :name => "MULTIPOINT" }, 26 | :multi_line_string => { :name => "MULTILINESTRING" }, 27 | :multi_polygon => { :name => "MULTIPOLYGON" }, 28 | :geometry => { :name => "GEOMETRY"} 29 | } 30 | end 31 | 32 | class NotCompatibleError < ::StandardError 33 | end 34 | end 35 | 36 | require 'spatial_adapter/common' 37 | require 'spatial_adapter/railtie' if defined?(Rails::Railtie) 38 | -------------------------------------------------------------------------------- /spec/models/common.rb: -------------------------------------------------------------------------------- 1 | class PointModel < ActiveRecord::Base 2 | end 3 | 4 | class LineStringModel < ActiveRecord::Base 5 | end 6 | 7 | class PolygonModel < ActiveRecord::Base 8 | end 9 | 10 | class MultiPointModel < ActiveRecord::Base 11 | end 12 | 13 | class MultiLineStringModel < ActiveRecord::Base 14 | end 15 | 16 | class MultiPolygonModel < ActiveRecord::Base 17 | end 18 | 19 | class GeometryCollectionModel < ActiveRecord::Base 20 | end 21 | 22 | class GeometryModel < ActiveRecord::Base 23 | end 24 | 25 | class PointzModel < ActiveRecord::Base 26 | end 27 | 28 | class PointmModel < ActiveRecord::Base 29 | end 30 | 31 | class Point4Model < ActiveRecord::Base 32 | end 33 | 34 | class GeographyPointModel < ActiveRecord::Base 35 | end 36 | 37 | class GeographyLineStringModel < ActiveRecord::Base 38 | end 39 | 40 | class GeographyPolygonModel < ActiveRecord::Base 41 | end 42 | 43 | class GeographyMultiPointModel < ActiveRecord::Base 44 | end 45 | 46 | class GeographyMultiLineStringModel < ActiveRecord::Base 47 | end 48 | 49 | class GeographyMultiPolygonModel < ActiveRecord::Base 50 | end 51 | 52 | class GeographyGeometryCollectionModel < ActiveRecord::Base 53 | end 54 | 55 | class GeographyModel < ActiveRecord::Base 56 | end 57 | 58 | class GeographyPointzModel < ActiveRecord::Base 59 | end 60 | 61 | class GeographyPointmModel < ActiveRecord::Base 62 | end 63 | 64 | class GeographyPoint4Model < ActiveRecord::Base 65 | end 66 | -------------------------------------------------------------------------------- /spatial_adapter.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require 'spatial_adapter/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{spatial_adapter} 7 | s.version = SpatialAdapter::VERSION 8 | 9 | s.platform = $platform || RUBY_PLATFORM[/java/] || Gem::Platform::RUBY 10 | 11 | s.authors = ["Pete Deffendol", "Guilhem Vellut"] 12 | s.email = %q{pete@fragility.us} 13 | s.homepage = %q{http://github.com/fragility/spatial_adapter} 14 | s.summary = "spatial_adapter-#{SpatialAdapter::VERSION}" 15 | s.description = %q{Provides enhancements to ActiveRecord to handle spatial 16 | datatypes in PostgreSQL and MySQL.} 17 | 18 | s.files = `git ls-files`.split("\n") 19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 21 | s.extra_rdoc_files = ["README.rdoc", "MIT-LICENSE"] 22 | s.rdoc_options = ["--charset=UTF-8"] 23 | s.require_paths = ["lib"] 24 | 25 | s.add_development_dependency 'rake' 26 | s.add_development_dependency 'rspec' 27 | if s.platform.to_s == 'ruby' 28 | s.add_development_dependency 'pg' 29 | s.add_development_dependency 'mysql' 30 | s.add_development_dependency 'mysql2', '<= 0.2.13' 31 | end 32 | 33 | if s.platform.to_s == 'java' 34 | s.add_development_dependency 'activerecord-jdbcmysql-adapter' 35 | end 36 | 37 | s.add_dependency 'activerecord', '>= 2.2.2', '< 3.1.0' 38 | s.add_dependency 'GeoRuby', '>= 1.3.0' 39 | end 40 | -------------------------------------------------------------------------------- /spec/shared/mysql_schema_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'spatially enabled schema dump' do 2 | let(:establish){ mysql_connection } 3 | 4 | let(:connection) do 5 | establish 6 | ActiveRecord::Base.connection 7 | end 8 | 9 | before :all do 10 | ActiveRecord::Schema.define do 11 | create_table :migrated_geometry_models, :options=> "ENGINE=MyISAM", :force => true do |t| 12 | t.integer :extra 13 | t.point :geom, :null => false 14 | end 15 | add_index :migrated_geometry_models, :geom, :spatial => true, 16 | :name => 'test_spatial_index' 17 | end 18 | 19 | File.open('schema.rb', "w:UTF-8") do |file| 20 | ActiveRecord::SchemaDumper.dump(connection, file) 21 | end 22 | 23 | connection.drop_table "migrated_geometry_models" 24 | 25 | load('schema.rb') 26 | end 27 | 28 | after :all do 29 | File.delete('schema.rb') 30 | 31 | connection.drop_table "migrated_geometry_models" 32 | end 33 | 34 | it "should preserve spatial attributes of tables" do 35 | columns = connection.columns("migrated_geometry_models") 36 | 37 | columns.should have(3).items 38 | geom_column = columns.select{|c| c.name == 'geom'}.first 39 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 40 | geom_column.geometry_type.should == :point 41 | geom_column.type.should == :string 42 | end 43 | 44 | it "should preserve spatial indexes" do 45 | indexes = connection.indexes("migrated_geometry_models") 46 | 47 | indexes.should have(1).item 48 | 49 | indexes.first.name.should == 'test_spatial_index' 50 | indexes.first.columns.should == ["geom"] 51 | indexes.first.spatial.should == true 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/spatial_adapter/mysql2.rb: -------------------------------------------------------------------------------- 1 | require 'spatial_adapter' 2 | require 'spatial_adapter/base/mysql' 3 | require 'active_record/connection_adapters/mysql2_adapter' 4 | 5 | module ActiveRecord::ConnectionAdapters 6 | class SpatialMysql2Column < Mysql2Column 7 | include SpatialAdapter::SpatialColumn 8 | extend SpatialAdapter::Base::Mysql::SpatialColumn 9 | end 10 | 11 | class Mysql2Adapter 12 | include SpatialAdapter::Base::Mysql::Adapter 13 | 14 | #Redefinition of columns to add the information that a column is geometric 15 | def columns(table_name, name = nil)#:nodoc: 16 | show_fields_from(table_name, name).map do |field| 17 | klass = \ 18 | if field[1] =~ GEOMETRY_REGEXP 19 | ActiveRecord::ConnectionAdapters::SpatialMysql2Column 20 | else 21 | ActiveRecord::ConnectionAdapters::Mysql2Column 22 | end 23 | klass.new(field[0], field[4], field[1], field[2] == "YES") 24 | end 25 | end 26 | 27 | # Check the nature of the index : If it is SPATIAL, it is indicated in the 28 | # IndexDefinition object (redefined to add the spatial flag in 29 | # spatial_adapter_common.rb) 30 | def indexes(table_name, name = nil)#:nodoc: 31 | indexes = [] 32 | current_index = nil 33 | show_keys_from(table_name, name).each do |row| 34 | if current_index != row[2] 35 | next if row[2] == "PRIMARY" # skip the primary key 36 | current_index = row[2] 37 | indexes << ActiveRecord::ConnectionAdapters::IndexDefinition \ 38 | .new(row[0], row[2], row[1] == "0", [], row[10] == "SPATIAL") 39 | end 40 | indexes.last.columns << row[4] 41 | end 42 | indexes 43 | end 44 | 45 | def options_for(table) 46 | engine = show_table_status_like(table).first[1] 47 | engine !~ /inno/i ? "ENGINE=#{engine}" : nil 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/spatial_adapter/jdbcmysql.rb: -------------------------------------------------------------------------------- 1 | require 'spatial_adapter' 2 | require 'spatial_adapter/base/mysql' 3 | require 'active_record/connection_adapters/jdbcmysql_adapter' 4 | 5 | module ActiveRecord::ConnectionAdapters 6 | class MysqlAdapter 7 | include SpatialAdapter::Base::Mysql::Adapter 8 | 9 | #Redefinition of columns to add the information that a column is geometric 10 | def columns(table_name, name = nil)#:nodoc: 11 | show_fields_from(table_name, name).map do |field| 12 | klass = \ 13 | if field["Type"] =~ GEOMETRY_REGEXP 14 | ActiveRecord::ConnectionAdapters::SpatialMysqlColumn 15 | else 16 | ActiveRecord::ConnectionAdapters::MysqlColumn 17 | end 18 | klass.new(field['Field'], field['Default'], field['Type'], field['Null'] == "YES") 19 | end 20 | end 21 | 22 | # Check the nature of the index : If it is SPATIAL, it is indicated in the 23 | # IndexDefinition object (redefined to add the spatial flag in 24 | # spatial_adapter_common.rb) 25 | def indexes(table_name, name = nil)#:nodoc: 26 | indexes = [] 27 | current_index = nil 28 | show_keys_from(table_name, name).each do |row| 29 | if current_index != row['Key_name'] 30 | next if row['Key_name'] == "PRIMARY" # skip the primary key 31 | current_index = row['Key_name'] 32 | indexes << ActiveRecord::ConnectionAdapters::IndexDefinition \ 33 | .new(row['Table'], row['Key_name'], row['Non_unique'] == "0", [], row['Index_type'] == "SPATIAL") 34 | end 35 | indexes.last.columns << row['Column_name'] 36 | end 37 | indexes 38 | end 39 | 40 | def options_for(table) 41 | engine = show_table_status_like(table).first['Engine'] 42 | engine !~ /inno/i ? "ENGINE=#{engine}" : nil 43 | end 44 | end 45 | 46 | class SpatialMysqlColumn < MysqlColumn 47 | include SpatialAdapter::SpatialColumn 48 | extend SpatialAdapter::Base::Mysql::SpatialColumn 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/spatial_adapter/mysql.rb: -------------------------------------------------------------------------------- 1 | require 'spatial_adapter' 2 | require 'spatial_adapter/base/mysql' 3 | require 'active_record/connection_adapters/mysql_adapter' 4 | 5 | module ActiveRecord::ConnectionAdapters 6 | class MysqlAdapter 7 | include SpatialAdapter::Base::Mysql::Adapter 8 | 9 | #Redefinition of columns to add the information that a column is geometric 10 | def columns(table_name, name = nil)#:nodoc: 11 | result = show_fields_from(table_name, name) 12 | 13 | columns = [] 14 | result.each do |field| 15 | klass = \ 16 | if field[1] =~ GEOMETRY_REGEXP 17 | ActiveRecord::ConnectionAdapters::SpatialMysqlColumn 18 | else 19 | ActiveRecord::ConnectionAdapters::MysqlColumn 20 | end 21 | columns << klass.new(field[0], field[4], field[1], field[2] == "YES") 22 | end 23 | 24 | result.free 25 | columns 26 | end 27 | 28 | # Check the nature of the index : If it is SPATIAL, it is indicated in the 29 | # IndexDefinition object (redefined to add the spatial flag in 30 | # spatial_adapter_common.rb) 31 | def indexes(table_name, name = nil)#:nodoc: 32 | indexes = [] 33 | current_index = nil 34 | show_keys_from(table_name, name).each do |row| 35 | if current_index != row[2] 36 | next if row[2] == "PRIMARY" # skip the primary key 37 | current_index = row[2] 38 | indexes << ActiveRecord::ConnectionAdapters::IndexDefinition \ 39 | .new(row[0], row[2], row[1] == "0", [], row[10] == "SPATIAL") 40 | end 41 | indexes.last.columns << row[4] 42 | end 43 | indexes 44 | end 45 | 46 | def options_for(table) 47 | engine = show_table_status_like(table).fetch_row[1] 48 | engine !~ /inno/i ? "ENGINE=#{engine}" : nil 49 | end 50 | end 51 | 52 | class SpatialMysqlColumn < MysqlColumn 53 | include SpatialAdapter::SpatialColumn 54 | extend SpatialAdapter::Base::Mysql::SpatialColumn 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/spatial_adapter/base/mysql/adapter.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter::Base::Mysql 2 | module Adapter 3 | GEOMETRY_REGEXP = /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i 4 | 5 | def supports_geographic? 6 | false 7 | end 8 | 9 | def self.included klass 10 | klass.class_eval do 11 | def native_database_types 12 | (defined?(self.class::NATIVE_DATABASE_TYPES) ? self.class::NATIVE_DATABASE_TYPES : super) \ 13 | .merge(SpatialAdapter.geometry_data_types) 14 | end 15 | 16 | # Redefines the quote method to add behaviour for when a Geometry is 17 | # encountered ; used when binding variables in find_by methods 18 | def quote(value, column = nil) 19 | if value.kind_of?(GeoRuby::SimpleFeatures::Geometry) 20 | "GeomFromWKB(0x#{value.as_hex_wkb},#{value.srid})" 21 | else 22 | super(value,column) 23 | end 24 | end 25 | 26 | #Redefines add_index to support the case where the index is spatial 27 | #If the :spatial key in the options table is true, then the sql string for a spatial index is created 28 | def add_index(table_name,column_name,options = {}) 29 | index_name = options[:name] || index_name(table_name,:column => Array(column_name)) 30 | 31 | if options[:spatial] 32 | execute "CREATE SPATIAL INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})" 33 | else 34 | super 35 | end 36 | end 37 | end 38 | end 39 | 40 | private 41 | 42 | def show_table_status_like(table) 43 | execute("SHOW TABLE STATUS LIKE '#{table}'") 44 | end 45 | 46 | def show_fields_from(table, name = nil) 47 | execute("SHOW FIELDS FROM #{quote_table_name(table)}", name) 48 | end 49 | 50 | def show_keys_from(table, name = nil) 51 | execute("SHOW KEYS FROM #{quote_table_name(table)}", name) || [] 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/shared/common_model_actions_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'common model actions' do 2 | context 'finding records' do 3 | it 'should retrieve Point objects' do 4 | model = PointModel.create(:extra => 'test', :geom => GeometryFactory.point) 5 | PointModel.find(model.id).geom.should == GeometryFactory.point 6 | end 7 | 8 | it 'should retrieve LineString objects' do 9 | model = LineStringModel.create(:extra => 'test', :geom => GeometryFactory.line_string) 10 | LineStringModel.find(model.id).geom.should == GeometryFactory.line_string 11 | end 12 | 13 | it 'should retrieve Polygon objects' do 14 | model = PolygonModel.create(:extra => 'test', :geom => GeometryFactory.polygon) 15 | PolygonModel.find(model.id).geom.should == GeometryFactory.polygon 16 | end 17 | 18 | it 'should retrieve MultiPoint objects' do 19 | model = MultiPointModel.create(:extra => 'test', :geom => GeometryFactory.multi_point) 20 | MultiPointModel.find(model.id).geom.should == GeometryFactory.multi_point 21 | end 22 | 23 | it 'should retrieve MultiLineString objects' do 24 | model = MultiLineStringModel.create(:extra => 'test', :geom => GeometryFactory.multi_line_string) 25 | MultiLineStringModel.find(model.id).geom.should == GeometryFactory.multi_line_string 26 | end 27 | 28 | it 'should retrieve MultiPolygon objects' do 29 | model = MultiPolygonModel.create(:extra => 'test', :geom => GeometryFactory.multi_polygon) 30 | MultiPolygonModel.find(model.id).geom.should == GeometryFactory.multi_polygon 31 | end 32 | 33 | it 'should retrieve GeometryCollection objects' do 34 | model = GeometryCollectionModel.create(:extra => 'test', :geom => GeometryFactory.geometry_collection) 35 | GeometryCollectionModel.find(model.id).geom.should == GeometryFactory.geometry_collection 36 | end 37 | 38 | it 'should retrieve Geometry objects' do 39 | model = GeometryModel.create(:extra => 'test', :geom => GeometryFactory.point) 40 | GeometryModel.find(model.id).geom.should == GeometryFactory.point 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/shared/mysql_migration_spec.rb: -------------------------------------------------------------------------------- 1 | class MigratedGeometryModel < ActiveRecord::Base 2 | end 3 | 4 | shared_examples_for 'spatially enabled migrations' do 5 | let(:establish){ mysql_connection } 6 | 7 | let(:connection) do 8 | establish 9 | ActiveRecord::Base.connection 10 | end 11 | 12 | context "creating tables" do 13 | after :each do 14 | connection.drop_table "migrated_geometry_models" 15 | end 16 | 17 | SpatialAdapter.geometry_data_types.keys.each do |type| 18 | it "should create #{type.to_s} columns" do 19 | ActiveRecord::Schema.define do 20 | create_table :migrated_geometry_models, :force => true do |t| 21 | t.integer :extra 22 | t.send(type, :geom) 23 | end 24 | end 25 | 26 | geom_column = connection \ 27 | .columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 28 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 29 | geom_column.geometry_type.should == type 30 | geom_column.type.should == :string 31 | end 32 | end 33 | end 34 | 35 | context "adding columns" do 36 | before :each do 37 | ActiveRecord::Schema.define do 38 | create_table :migrated_geometry_models, :force => true do |t| 39 | t.integer :extra 40 | end 41 | end 42 | end 43 | 44 | after :each do 45 | connection.drop_table "migrated_geometry_models" 46 | end 47 | 48 | SpatialAdapter.geometry_data_types.keys.each do |type| 49 | it "should add #{type.to_s} columns" do 50 | ActiveRecord::Schema.define do 51 | add_column :migrated_geometry_models, :geom, type 52 | end 53 | 54 | geom_column = connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 55 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 56 | geom_column.geometry_type.should == type 57 | geom_column.type.should == :string 58 | geom_column.with_z.should == false 59 | geom_column.with_m.should == false 60 | geom_column.srid.should == -1 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/db/jdbcmysql_raw.rb: -------------------------------------------------------------------------------- 1 | jdbcmysql_connection 2 | 3 | ActiveRecord::Schema.define() do 4 | execute "drop table if exists point_models" 5 | execute "create table point_models 6 | ( 7 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 8 | extra varchar(100), 9 | more_extra varchar(100), 10 | geom point not null 11 | ) ENGINE=MyISAM" 12 | execute "create spatial index index_point_models_on_geom on point_models (geom)" 13 | execute "create index index_point_models_on_extra on point_models (extra, more_extra)" 14 | 15 | execute "drop table if exists line_string_models" 16 | execute "create table line_string_models 17 | ( 18 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 19 | extra varchar(100), 20 | geom linestring 21 | ) ENGINE=MyISAM" 22 | 23 | execute "drop table if exists polygon_models" 24 | execute "create table polygon_models 25 | ( 26 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 27 | extra varchar(100), 28 | geom polygon 29 | ) ENGINE=MyISAM" 30 | 31 | execute "drop table if exists multi_point_models" 32 | execute "create table multi_point_models 33 | ( 34 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 35 | extra varchar(100), 36 | geom multipoint 37 | ) ENGINE=MyISAM" 38 | 39 | execute "drop table if exists multi_line_string_models" 40 | execute "create table multi_line_string_models 41 | ( 42 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 43 | extra varchar(100), 44 | geom multilinestring 45 | ) ENGINE=MyISAM" 46 | 47 | execute "drop table if exists multi_polygon_models" 48 | execute "create table multi_polygon_models 49 | ( 50 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 51 | extra varchar(100), 52 | geom multipolygon 53 | ) ENGINE=MyISAM" 54 | 55 | execute "drop table if exists geometry_collection_models" 56 | execute "create table geometry_collection_models 57 | ( 58 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 59 | extra varchar(100), 60 | geom geometrycollection 61 | ) ENGINE=MyISAM" 62 | 63 | execute "drop table if exists geometry_models" 64 | execute "create table geometry_models 65 | ( 66 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 67 | extra varchar(100), 68 | geom geometry 69 | ) ENGINE=MyISAM" 70 | end 71 | -------------------------------------------------------------------------------- /spec/db/mysql2_raw.rb: -------------------------------------------------------------------------------- 1 | mysql2_connection 2 | 3 | ActiveRecord::Schema.define() do 4 | execute "drop table if exists point_models" 5 | execute "create table point_models 6 | ( 7 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 8 | extra varchar(100), 9 | more_extra varchar(100), 10 | geom point not null 11 | ) ENGINE=MyISAM" 12 | execute "create spatial index index_point_models_on_geom on point_models (geom)" 13 | execute "create index index_point_models_on_extra on point_models (extra, more_extra)" 14 | 15 | execute "drop table if exists line_string_models" 16 | execute "create table line_string_models 17 | ( 18 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 19 | extra varchar(100), 20 | geom linestring 21 | ) ENGINE=MyISAM" 22 | 23 | execute "drop table if exists polygon_models" 24 | execute "create table polygon_models 25 | ( 26 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 27 | extra varchar(100), 28 | geom polygon 29 | ) ENGINE=MyISAM" 30 | 31 | execute "drop table if exists multi_point_models" 32 | execute "create table multi_point_models 33 | ( 34 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 35 | extra varchar(100), 36 | geom multipoint 37 | ) ENGINE=MyISAM" 38 | 39 | execute "drop table if exists multi_line_string_models" 40 | execute "create table multi_line_string_models 41 | ( 42 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 43 | extra varchar(100), 44 | geom multilinestring 45 | ) ENGINE=MyISAM" 46 | 47 | execute "drop table if exists multi_polygon_models" 48 | execute "create table multi_polygon_models 49 | ( 50 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 51 | extra varchar(100), 52 | geom multipolygon 53 | ) ENGINE=MyISAM" 54 | 55 | execute "drop table if exists geometry_collection_models" 56 | execute "create table geometry_collection_models 57 | ( 58 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 59 | extra varchar(100), 60 | geom geometrycollection 61 | ) ENGINE=MyISAM" 62 | 63 | execute "drop table if exists geometry_models" 64 | execute "create table geometry_models 65 | ( 66 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 67 | extra varchar(100), 68 | geom geometry 69 | ) ENGINE=MyISAM" 70 | end 71 | -------------------------------------------------------------------------------- /spec/db/mysql_raw.rb: -------------------------------------------------------------------------------- 1 | mysql_connection 2 | 3 | ActiveRecord::Schema.define() do 4 | execute "drop table if exists point_models" 5 | execute "create table point_models 6 | ( 7 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 8 | extra varchar(100), 9 | more_extra varchar(100), 10 | geom point not null 11 | ) ENGINE=MyISAM" 12 | execute "create spatial index index_point_models_on_geom on point_models (geom)" 13 | execute "create index index_point_models_on_extra on point_models (extra, more_extra)" 14 | 15 | execute "drop table if exists line_string_models" 16 | execute "create table line_string_models 17 | ( 18 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 19 | extra varchar(100), 20 | geom linestring 21 | ) ENGINE=MyISAM" 22 | 23 | execute "drop table if exists polygon_models" 24 | execute "create table polygon_models 25 | ( 26 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 27 | extra varchar(100), 28 | geom polygon 29 | ) ENGINE=MyISAM" 30 | 31 | execute "drop table if exists multi_point_models" 32 | execute "create table multi_point_models 33 | ( 34 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 35 | extra varchar(100), 36 | geom multipoint 37 | ) ENGINE=MyISAM" 38 | 39 | execute "drop table if exists multi_line_string_models" 40 | execute "create table multi_line_string_models 41 | ( 42 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 43 | extra varchar(100), 44 | geom multilinestring 45 | ) ENGINE=MyISAM" 46 | 47 | execute "drop table if exists multi_polygon_models" 48 | execute "create table multi_polygon_models 49 | ( 50 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 51 | extra varchar(100), 52 | geom multipolygon 53 | ) ENGINE=MyISAM" 54 | 55 | execute "drop table if exists geometry_collection_models" 56 | execute "create table geometry_collection_models 57 | ( 58 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 59 | extra varchar(100), 60 | geom geometrycollection 61 | ) ENGINE=MyISAM" 62 | 63 | execute "drop table if exists geometry_models" 64 | execute "create table geometry_models 65 | ( 66 | id int(11) DEFAULT NULL auto_increment PRIMARY KEY, 67 | extra varchar(100), 68 | geom geometry 69 | ) ENGINE=MyISAM" 70 | end 71 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common/spatial_column.rb: -------------------------------------------------------------------------------- 1 | module SpatialAdapter 2 | module SpatialColumn 3 | attr_reader :geometry_type, :srid, :with_z, :with_m 4 | 5 | def initialize(name, default, sql_type = nil, null = true, srid=-1, with_z=false, with_m=false) 6 | super(name, default, sql_type, null) 7 | @geometry_type = geometry_simplified_type(@sql_type) 8 | @srid = srid 9 | @with_z = with_z 10 | @with_m = with_m 11 | end 12 | 13 | def spatial? 14 | !@geometry_type.nil? 15 | end 16 | 17 | def geographic? 18 | false 19 | end 20 | 21 | # Redefines type_cast to add support for geometries 22 | # alias_method :type_cast_without_spatial, :type_cast 23 | def type_cast(value) 24 | return nil if value.nil? 25 | spatial? ? self.class.string_to_geometry(value) : super 26 | end 27 | 28 | #Redefines type_cast_code to add support for geometries. 29 | # 30 | #WARNING : Since ActiveRecord keeps only the string values directly returned from the database, it translates from these to the correct types everytime an attribute is read (using the code returned by this method), which is probably ok for simple types, but might be less than efficient for geometries. Also you cannot modify the geometry object returned directly or your change will not be saved. 31 | # alias_method :type_cast_code_without_spatial, :type_cast_code 32 | def type_cast_code(var_name) 33 | spatial? ? "#{self.class.name}.string_to_geometry(#{var_name})" : super 34 | end 35 | 36 | 37 | #Redefines klass to add support for geometries 38 | # alias_method :klass_without_spatial, :klass 39 | def klass 40 | spatial? ? GeoRuby::SimpleFeatures::Geometry : super 41 | end 42 | 43 | private 44 | 45 | # Maps additional data types to base Rails/Arel types 46 | # 47 | # For Rails 3, only the types defined by Arel can be used. We'll 48 | # use :string since the database returns the columns as hex strings. 49 | def simplified_type(field_type) 50 | case field_type 51 | when /geography|geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :string 52 | else super 53 | end 54 | end 55 | 56 | # less simlpified geometric type to be use in migrations 57 | def geometry_simplified_type(sql_type) 58 | case sql_type 59 | when /^point$/i then :point 60 | when /^linestring$/i then :line_string 61 | when /^polygon$/i then :polygon 62 | when /^geometry$/i then :geometry 63 | when /multipoint/i then :multi_point 64 | when /multilinestring/i then :multi_line_string 65 | when /multipolygon/i then :multi_polygon 66 | when /geometrycollection/i then :geometry_collection 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/postgresql/schema_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spatial_adapter/postgresql' 3 | 4 | describe "Spatially-enabled Schema Dumps" do 5 | before :all do 6 | postgis_connection 7 | @connection = ActiveRecord::Base.connection 8 | 9 | # Create a new table 10 | ActiveRecord::Schema.define do 11 | create_table :migrated_geometry_models, :force => true do |t| 12 | t.integer :extra 13 | t.point :geom, :with_m => true, :with_z => true, :srid => 4326 14 | end 15 | add_index :migrated_geometry_models, :geom, :spatial => true, :name => 'test_spatial_index' 16 | 17 | create_table :migrated_geography_models, :force => true do |t| 18 | t.integer :extra 19 | t.point :geom, :with_m => true, :with_z => true, :geographic => true 20 | end 21 | end 22 | 23 | File.open('schema.rb', "w:UTF-8") do |file| 24 | ActiveRecord::SchemaDumper.dump(@connection, file) 25 | end 26 | 27 | # Drop the original tables 28 | @connection.drop_table "migrated_geometry_models" 29 | @connection.drop_table "migrated_geography_models" 30 | 31 | # Load the dumped schema 32 | load('schema.rb') 33 | end 34 | 35 | after :all do 36 | # delete the schema file 37 | File.delete('schema.rb') 38 | 39 | # Drop the new tables 40 | @connection.drop_table "migrated_geometry_models" 41 | @connection.drop_table "migrated_geography_models" 42 | end 43 | 44 | it "should preserve spatial attributes of geometry tables" do 45 | columns = @connection.columns("migrated_geometry_models") 46 | 47 | columns.should have(3).items 48 | geom_column = columns.select{|c| c.name == 'geom'}.first 49 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 50 | geom_column.geometry_type.should == :point 51 | geom_column.type.should == :string 52 | geom_column.with_z.should == true 53 | geom_column.with_m.should == true 54 | geom_column.srid.should == 4326 55 | end 56 | 57 | it "should preserve spatial attributes of geography tables" do 58 | columns = @connection.columns("migrated_geography_models") 59 | 60 | columns.should have(3).items 61 | geom_column = columns.select{|c| c.name == 'geom'}.first 62 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 63 | geom_column.geometry_type.should == :point 64 | geom_column.type.should == :string 65 | geom_column.with_z.should == true 66 | geom_column.with_m.should == true 67 | geom_column.should be_geographic 68 | end 69 | 70 | it "should preserve spatial indexes" do 71 | indexes = @connection.indexes("migrated_geometry_models") 72 | 73 | indexes.should have(1).item 74 | 75 | indexes.first.name.should == 'test_spatial_index' 76 | indexes.first.columns.should == ["geom"] 77 | indexes.first.spatial.should == true 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require(:default) 3 | 4 | def postgis_connection 5 | ActiveRecord::Base.establish_connection( 6 | :adapter => 'postgresql', 7 | :database => 'spatial_adapter' 8 | ) 9 | # Turn off those annoying NOTICE messages 10 | ActiveRecord::Base.connection.execute 'set client_min_messages = warning' 11 | 12 | # Don't output migration logging 13 | ActiveRecord::Migration.verbose = false 14 | end 15 | 16 | def mysql_connection 17 | ActiveRecord::Base.establish_connection( 18 | :adapter => 'mysql', 19 | :database => 'spatial_adapter', 20 | :username => 'root', 21 | :host => 'localhost' 22 | ) 23 | 24 | # Don't output migration logging 25 | ActiveRecord::Migration.verbose = false 26 | end 27 | 28 | def mysql2_connection 29 | ActiveRecord::Base.establish_connection( 30 | :adapter => 'mysql2', 31 | :database => 'spatial_adapter', 32 | :username => 'root', 33 | :host => 'localhost' 34 | ) 35 | 36 | # Don't output migration logging 37 | ActiveRecord::Migration.verbose = false 38 | end 39 | 40 | def jdbcmysql_connection 41 | ActiveRecord::Base.establish_connection( 42 | :adapter => 'jdbcmysql', 43 | :database => 'spatial_adapter', 44 | :username => 'root', 45 | :host => 'localhost' 46 | ) 47 | 48 | # Don't output migration logging 49 | ActiveRecord::Migration.verbose = false 50 | end 51 | 52 | class GeometryFactory 53 | class << self 54 | def point 55 | Point.from_x_y(1, 2, 4326) 56 | end 57 | 58 | def line_string 59 | LineString.from_coordinates([[1.4,2.5],[1.5,6.7]], 4326) 60 | end 61 | 62 | def polygon 63 | Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]], 4326) 64 | end 65 | 66 | def multi_point 67 | MultiPoint.from_coordinates([[12.4,-23.3],[-65.1,23.4],[23.55555555,23]], 4326) 68 | end 69 | 70 | def multi_line_string 71 | MultiLineString.from_line_strings([LineString.from_coordinates([[1.5,45.2],[-54.12312,-0.012]]),LineString.from_coordinates([[1.5,45.2],[-54.12312,-0.012],[45.123,23.3]])], 4326) 72 | end 73 | 74 | def multi_polygon 75 | MultiPolygon.from_polygons([Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]]),Polygon.from_coordinates([[[0,0],[4,0],[4,4],[0,4],[0,0]],[[1,1],[3,1],[3,3],[1,3],[1,1]]])], 4326) 76 | end 77 | 78 | def geometry_collection 79 | GeometryCollection.from_geometries([Point.from_x_y(4.67,45.4),LineString.from_coordinates([[5.7,12.45],[67.55,54]])], 4326) 80 | end 81 | 82 | def pointz 83 | Point.from_x_y_z(1, 2, 3, 4326) 84 | end 85 | 86 | def pointm 87 | Point.from_x_y_m(1, 2, 3, 4326) 88 | end 89 | 90 | def point4 91 | Point.from_x_y_z_m(1, 2, 3, 4, 4326) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/shared/mysql_models_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'spatially enabled models' do 2 | let(:establish){ mysql_connection } 3 | 4 | let(:connection) do 5 | establish 6 | ActiveRecord::Base.connection 7 | end 8 | 9 | context "inserting records" do 10 | it 'should save Point objects' do 11 | model = PointModel.new(:extra => 'test', :geom => GeometryFactory.point) 12 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.point.as_hex_wkb), anything(), anything(), anything(), anything()) 13 | model.save.should == true 14 | end 15 | 16 | it 'should save LineString objects' do 17 | model = LineStringModel.new(:extra => 'test', :geom => GeometryFactory.line_string) 18 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.line_string.as_hex_wkb), anything(), anything(), anything(), anything()) 19 | model.save.should == true 20 | end 21 | 22 | it 'should save Polygon objects' do 23 | model = PolygonModel.new(:extra => 'test', :geom => GeometryFactory.polygon) 24 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.polygon.as_hex_wkb), anything(), anything(), anything(), anything()) 25 | model.save.should == true 26 | end 27 | 28 | it 'should save MultiPoint objects' do 29 | model = MultiPointModel.new(:extra => 'test', :geom => GeometryFactory.multi_point) 30 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.multi_point.as_hex_wkb), anything(), anything(), anything(), anything()) 31 | model.save.should == true 32 | end 33 | 34 | it 'should save MultiLineString objects' do 35 | model = MultiLineStringModel.new(:extra => 'test', :geom => GeometryFactory.multi_line_string) 36 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.multi_line_string.as_hex_wkb), anything(), anything(), anything(), anything()) 37 | model.save.should == true 38 | end 39 | 40 | it 'should save MultiPolygon objects' do 41 | model = MultiPolygonModel.new(:extra => 'test', :geom => GeometryFactory.multi_polygon) 42 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.multi_polygon.as_hex_wkb), anything(), anything(), anything(), anything()) 43 | model.save.should == true 44 | end 45 | 46 | it 'should save GeometryCollection objects' do 47 | model = GeometryCollectionModel.new(:extra => 'test', :geom => GeometryFactory.geometry_collection) 48 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.geometry_collection.as_hex_wkb), anything(), anything(), anything(), anything()) 49 | model.save.should == true 50 | end 51 | 52 | it 'should save Geometry objects' do 53 | model = GeometryModel.new(:extra => 'test', :geom => GeometryFactory.point) 54 | connection.should_receive(:insert_sql).with(Regexp.new(GeometryFactory.point.as_hex_wkb), anything(), anything(), anything(), anything()) 55 | model.save.should == true 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/shared/mysql_connection_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'a modified mysql adapter' do 2 | let(:establish){ mysql_connection } 3 | let(:column){ ActiveRecord::ConnectionAdapters::MysqlColumn } 4 | let(:spatial_column){ ActiveRecord::ConnectionAdapters::SpatialMysqlColumn } 5 | 6 | let(:connection) do 7 | establish 8 | ActiveRecord::Base.connection 9 | end 10 | 11 | context '#supports_geographic?' do 12 | it "should be false" do 13 | connection.supports_geographic?.should == false 14 | end 15 | end 16 | 17 | context "#columns" do 18 | context "type" do 19 | it "should be SpatialMysqlColumn if column is a spatial data type" do 20 | PointModel.columns.select{|c| c.name == 'geom'} \ 21 | .first.should be_a(spatial_column) 22 | end 23 | 24 | it "should be SpatialMysqlColumn if column is not a spatial data type" do 25 | PointModel.columns.select{|c| c.name == 'extra'} \ 26 | .first.should be_a(column) 27 | end 28 | end 29 | 30 | describe "@geometry_type" do 31 | it "should be :point for columns restricted to POINT types" do 32 | PointModel.columns.select{|c| c.name == 'geom'} \ 33 | .first.geometry_type.should == :point 34 | end 35 | 36 | it "should be :line_string for columns restricted to LINESTRING types" do 37 | LineStringModel.columns.select{|c| c.name == 'geom'} \ 38 | .first.geometry_type.should == :line_string 39 | end 40 | 41 | it "should be :polygon for columns restricted to POLYGON types" do 42 | PolygonModel.columns.select{|c| c.name == 'geom'} \ 43 | .first.geometry_type.should == :polygon 44 | end 45 | 46 | it "should be :multi_point for columns restricted to MULTIPOINT types" do 47 | MultiPointModel.columns.select{|c| c.name == 'geom'} \ 48 | .first.geometry_type.should == :multi_point 49 | end 50 | 51 | it "should be :multi_line_string for columns restricted to MULTILINESTRING types" do 52 | MultiLineStringModel.columns.select{|c| c.name == 'geom'} \ 53 | .first.geometry_type.should == :multi_line_string 54 | end 55 | 56 | it "should be :multi_polygon for columns restricted to MULTIPOLYGON types" do 57 | MultiPolygonModel.columns.select{|c| c.name == 'geom'} \ 58 | .first.geometry_type.should == :multi_polygon 59 | end 60 | 61 | it "should be :geometry_collection for columns restricted to GEOMETRYCOLLECTION types" do 62 | GeometryCollectionModel.columns.select{|c| c.name == 'geom'} \ 63 | .first.geometry_type.should == :geometry_collection 64 | end 65 | 66 | it "should be :geometry for columns not restricted to a type" do 67 | GeometryModel.columns.select{|c| c.name == 'geom'} \ 68 | .first.geometry_type.should == :geometry 69 | end 70 | end 71 | end 72 | 73 | context "#indexes" do 74 | let(:indexes){ connection.indexes('point_models') } 75 | 76 | it "should return an IndexDefinition for each index on the table" do 77 | indexes.should have(2).items 78 | indexes.each do |i| 79 | i.should be_a(ActiveRecord::ConnectionAdapters::IndexDefinition) 80 | end 81 | end 82 | 83 | it "should indicate the correct columns in the index" do 84 | indexes.select{|i| i.name == 'index_point_models_on_geom'} \ 85 | .first.columns.should == ['geom'] 86 | indexes.select{|i| i.name == 'index_point_models_on_extra'} \ 87 | .first.columns.should == ['extra', 'more_extra'] 88 | end 89 | 90 | it "should be marked as spatial if a spatial index" do 91 | indexes.select{|i| i.columns.include?('geom')}.first.spatial.should == true 92 | end 93 | 94 | it "should not be marked as spatial if not a spatial index" do 95 | indexes.select{|i| i.columns.include?('extra')}.first.spatial.should == false 96 | end 97 | end 98 | 99 | context "#add_index" do 100 | it "should create a spatial index given :spatial => true" do 101 | connection.should_receive(:execute).with(/create spatial index/i) 102 | connection.add_index('geometry_models', 'geom', :spatial => true) 103 | end 104 | 105 | it "should not create a spatial index unless specified" do 106 | connection.should_not_receive(:execute).with(/create spatial index/i) 107 | connection.add_index('geometry_models', 'extra') 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/spatial_adapter/common/schema_dumper.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::SchemaDumper.ignore_tables << "spatial_ref_sys" << "geometry_columns" 2 | 3 | ActiveRecord::SchemaDumper.class_eval do 4 | # These are the valid options for a column specification (spatial options added) 5 | VALID_COLUMN_SPEC_KEYS = [:name, :limit, :precision, :scale, :default, :null, :srid, :with_z, :with_m, :geographic] 6 | 7 | def table(table, stream) 8 | columns = @connection.columns(table) 9 | begin 10 | tbl = StringIO.new 11 | 12 | # first dump primary key column 13 | if @connection.respond_to?(:pk_and_sequence_for) 14 | pk, pk_seq = @connection.pk_and_sequence_for(table) 15 | elsif @connection.respond_to?(:primary_key) 16 | pk = @connection.primary_key(table) 17 | end 18 | 19 | tbl.print " create_table #{table.inspect}" 20 | if columns.detect { |c| c.name == pk } 21 | if pk != 'id' 22 | tbl.print %Q(, :primary_key => "#{pk}") 23 | end 24 | else 25 | tbl.print ", :id => false" 26 | end 27 | 28 | # Added by Spatial Adapter to ensure correct MySQL table engine 29 | if @connection.respond_to?(:options_for) 30 | res = @connection.options_for(table) 31 | tbl.print ", :options=>'#{res}'" if res 32 | end 33 | 34 | tbl.print ", :force => true" 35 | tbl.puts " do |t|" 36 | 37 | # then dump all non-primary key columns 38 | column_specs = columns.map do |column| 39 | raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? 40 | next if column.name == pk 41 | spec = column_spec(column) 42 | (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} 43 | spec 44 | end.compact 45 | 46 | # find all migration keys used in this table 47 | keys = VALID_COLUMN_SPEC_KEYS & column_specs.map(&:keys).flatten 48 | 49 | # figure out the lengths for each column based on above keys 50 | lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } 51 | 52 | # the string we're going to sprintf our values against, with standardized column widths 53 | format_string = lengths.map{ |len| "%-#{len}s" } 54 | 55 | # find the max length for the 'type' column, which is special 56 | type_length = column_specs.map{ |column| column[:type].length }.max 57 | 58 | # add column type definition to our format string 59 | format_string.unshift " t.%-#{type_length}s " 60 | 61 | format_string *= '' 62 | 63 | column_specs.each do |colspec| 64 | values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } 65 | values.unshift colspec[:type] 66 | tbl.print((format_string % values).gsub(/,\s*$/, '')) 67 | tbl.puts 68 | end 69 | 70 | tbl.puts " end" 71 | tbl.puts 72 | 73 | indexes(table, tbl) 74 | 75 | tbl.rewind 76 | stream.print tbl.read 77 | rescue => e 78 | stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" 79 | stream.puts "# #{e.message}" 80 | stream.puts 81 | end 82 | 83 | stream 84 | end 85 | 86 | 87 | def indexes(table, stream) 88 | if (indexes = @connection.indexes(table)).any? 89 | add_index_statements = indexes.map do |index| 90 | statment_parts = [ ('add_index ' + index.table.inspect) ] 91 | statment_parts << index.columns.inspect 92 | statment_parts << (':name => ' + index.name.inspect) 93 | statment_parts << ':unique => true' if index.unique 94 | # Add spatial option (this is the only change from the original method) 95 | statment_parts << ':spatial => true' if index.spatial 96 | 97 | ' ' + statment_parts.join(', ') 98 | end 99 | 100 | stream.puts add_index_statements.sort.join("\n") 101 | stream.puts 102 | end 103 | end 104 | 105 | private 106 | 107 | # Build specification for a table column 108 | def column_spec(column) 109 | spec = {} 110 | spec[:name] = column.name.inspect 111 | 112 | # AR has an optimisation which handles zero-scale decimals as integers. This 113 | # code ensures that the dumper still dumps the column as a decimal. 114 | spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) } 115 | 'decimal' 116 | else 117 | column.type.to_s 118 | end 119 | spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' 120 | spec[:precision] = column.precision.inspect if !column.precision.nil? 121 | spec[:scale] = column.scale.inspect if !column.scale.nil? 122 | spec[:null] = 'false' if !column.null 123 | spec[:default] = default_string(column.default) if column.has_default? 124 | 125 | # Additions for spatial columns 126 | if column.is_a?(::SpatialAdapter::SpatialColumn) 127 | # Override with specific geometry type 128 | spec[:type] = column.geometry_type.to_s 129 | spec[:srid] = column.srid.inspect if column.srid != -1 130 | spec[:with_z] = 'true' if column.with_z 131 | spec[:with_m] = 'true' if column.with_m 132 | spec[:geographic] = 'true' if column.geographic? 133 | end 134 | spec 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/db/postgis_raw.rb: -------------------------------------------------------------------------------- 1 | postgis_connection 2 | 3 | ActiveRecord::Schema.define() do 4 | execute <<-SQL 5 | drop table if exists point_models; 6 | create table point_models 7 | ( 8 | id serial primary key, 9 | extra varchar(255), 10 | more_extra varchar(255) 11 | ); 12 | select AddGeometryColumn('point_models', 'geom', 4326, 'POINT', 2); 13 | create index index_point_models_on_geom on point_models using gist (geom); 14 | create index index_point_models_on_extra on point_models (extra, more_extra); 15 | 16 | drop table if exists line_string_models; 17 | create table line_string_models 18 | ( 19 | id serial primary key, 20 | extra varchar(255) 21 | ); 22 | select AddGeometryColumn('line_string_models', 'geom', 4326, 'LINESTRING', 2); 23 | 24 | drop table if exists polygon_models; 25 | create table polygon_models 26 | ( 27 | id serial primary key, 28 | extra varchar(255) 29 | ); 30 | select AddGeometryColumn('polygon_models', 'geom', 4326, 'POLYGON', 2); 31 | 32 | drop table if exists multi_point_models; 33 | create table multi_point_models 34 | ( 35 | id serial primary key, 36 | extra varchar(255) 37 | ); 38 | select AddGeometryColumn('multi_point_models', 'geom', 4326, 'MULTIPOINT', 2); 39 | 40 | drop table if exists multi_line_string_models; 41 | create table multi_line_string_models 42 | ( 43 | id serial primary key, 44 | extra varchar(255) 45 | ); 46 | select AddGeometryColumn('multi_line_string_models', 'geom', 4326, 'MULTILINESTRING', 2); 47 | 48 | drop table if exists multi_polygon_models; 49 | create table multi_polygon_models 50 | ( 51 | id serial primary key, 52 | extra varchar(255) 53 | ); 54 | select AddGeometryColumn('multi_polygon_models', 'geom', 4326, 'MULTIPOLYGON', 2); 55 | 56 | drop table if exists geometry_collection_models; 57 | create table geometry_collection_models 58 | ( 59 | id serial primary key, 60 | extra varchar(255) 61 | ); 62 | select AddGeometryColumn('geometry_collection_models', 'geom', 4326, 'GEOMETRYCOLLECTION', 2); 63 | 64 | drop table if exists geometry_models; 65 | create table geometry_models 66 | ( 67 | id serial primary key, 68 | extra varchar(255) 69 | ); 70 | select AddGeometryColumn('geometry_models', 'geom', 4326, 'GEOMETRY', 2); 71 | 72 | drop table if exists pointz_models; 73 | create table pointz_models 74 | ( 75 | id serial primary key, 76 | extra varchar(255) 77 | ); 78 | select AddGeometryColumn('pointz_models', 'geom', 4326, 'POINT', 3); 79 | 80 | drop table if exists pointm_models; 81 | create table pointm_models 82 | ( 83 | id serial primary key, 84 | extra varchar(255) 85 | ); 86 | select AddGeometryColumn('pointm_models', 'geom', 4326, 'POINTM', 3); 87 | 88 | drop table if exists point4_models; 89 | create table point4_models 90 | ( 91 | id serial primary key, 92 | extra varchar(255) 93 | ); 94 | select AddGeometryColumn('point4_models', 'geom', 4326, 'POINT', 4); 95 | SQL 96 | 97 | if ActiveRecord::Base.connection.supports_geographic? 98 | execute <<-SQL 99 | drop table if exists geography_point_models; 100 | create table geography_point_models 101 | ( 102 | id serial primary key, 103 | extra varchar(255), 104 | geom geography(POINT) 105 | ); 106 | create index index_geography_point_models_on_geom on geography_point_models using gist (geom); 107 | create index index_geography_point_models_on_extra on geography_point_models (extra); 108 | 109 | drop table if exists geography_line_string_models; 110 | create table geography_line_string_models 111 | ( 112 | id serial primary key, 113 | extra varchar(255), 114 | geom geography(LINESTRING) 115 | ); 116 | 117 | drop table if exists geography_polygon_models; 118 | create table geography_polygon_models 119 | ( 120 | id serial primary key, 121 | extra varchar(255), 122 | geom geography(POLYGON) 123 | ); 124 | 125 | drop table if exists geography_multi_point_models; 126 | create table geography_multi_point_models 127 | ( 128 | id serial primary key, 129 | extra varchar(255), 130 | geom geography(MULTIPOINT) 131 | ); 132 | 133 | drop table if exists geography_multi_line_string_models; 134 | create table geography_multi_line_string_models 135 | ( 136 | id serial primary key, 137 | extra varchar(255), 138 | geom geography(MULTILINESTRING) 139 | ); 140 | 141 | drop table if exists geography_multi_polygon_models; 142 | create table geography_multi_polygon_models 143 | ( 144 | id serial primary key, 145 | extra varchar(255), 146 | geom geography(MULTIPOLYGON) 147 | ); 148 | 149 | drop table if exists geography_geometry_collection_models; 150 | create table geography_geometry_collection_models 151 | ( 152 | id serial primary key, 153 | extra varchar(255), 154 | geom geography(GEOMETRYCOLLECTION) 155 | ); 156 | 157 | drop table if exists geography_models; 158 | create table geography_models 159 | ( 160 | id serial primary key, 161 | extra varchar(255), 162 | geom geography 163 | ); 164 | 165 | drop table if exists geography_pointz_models; 166 | create table geography_pointz_models 167 | ( 168 | id serial primary key, 169 | extra varchar(255), 170 | geom geography(POINTZ) 171 | ); 172 | 173 | drop table if exists geography_pointm_models; 174 | create table geography_pointm_models 175 | ( 176 | id serial primary key, 177 | extra varchar(255), 178 | geom geography(POINTM) 179 | ); 180 | 181 | drop table if exists geography_point4_models; 182 | create table geography_point4_models 183 | ( 184 | id serial primary key, 185 | extra varchar(255), 186 | geom geography(POINTZM) 187 | ); 188 | SQL 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Spatial Adapter for ActiveRecord 2 | 3 | This is the Spatial Adapter for ActiveRecord. It enhances ActiveRecord to 4 | handle spatial datatypes in the following databases: 5 | 6 | - PostgreSQL (using PostGIS) 7 | - MySQL (using Spatial Extensions) 8 | 9 | == Dependencies 10 | 11 | The following gems are required: 12 | 13 | - GeoRuby 14 | - ActiveRecord (version 2.2.2 and up) 15 | 16 | For PostgreSQL: 17 | 18 | - PostGIS version 1.4.0 or higher should be installed in your database 19 | 20 | == Installation 21 | 22 | Choose ONE of the following installation methods. You shouldn't have to do both. 23 | 24 | === From RubyGems 25 | 26 | This is the preferred method of installation, and will pull in the required 27 | dependencies as well. 28 | 29 | gem install spatial_adapter 30 | 31 | In a Rails 2.x app, you can add a gem dependency in environment.rb: 32 | 33 | config.gem 'spatial_adapter' 34 | 35 | In a Rails 3 app, add a gem dependency to Gemfile: 36 | 37 | gem 'spatial_adapter' 38 | 39 | === As a Rails Plugin 40 | 41 | In your Rails project, run the following: 42 | 43 | script/plugin install git://github.com/fragility/spatial_adapter.git 44 | 45 | You need to have Git installed first. 46 | 47 | == Configuration 48 | 49 | Choose the database type for which you would like to use spatial_adapter, and 50 | load each with 51 | 52 | require 'spatial_adapter/[database]' 53 | 54 | where [database] should be replaced with one of the following: 55 | 56 | - postgresql 57 | - mysql 58 | - mysql2 59 | - jdbcmysql 60 | 61 | For example to use the PostgreSQL spatial adapter: 62 | 63 | require 'spatial_adapter/postgresql' 64 | 65 | In a Rails app, spatial_adapter will automatically load the adapter for the 66 | database specified in your database.yml configuration. 67 | 68 | == Operations 69 | 70 | Geometric columns in your ActiveRecord models now appear just like any other 71 | column of other basic data types. They can also be dumped in ruby schema mode 72 | and loaded in migrations the same way as columns of basic types. 73 | 74 | === Migrations 75 | 76 | Here is an example of code for the creation of a table with a geometric column 77 | in PostGIS, along with the addition of a spatial index on the column: 78 | 79 | ActiveRecord::Schema.define do 80 | create_table :table_points, :force => true do |t| 81 | t.string :data 82 | t.point :geom, :null => false, :srid => 123, :with_z => true 83 | end 84 | 85 | add_index :table_points, :geom, :spatial => true 86 | end 87 | 88 | Here is a related statement valid for MySql version <= 5.0.16: 89 | 90 | ActiveRecord::Schema.define do 91 | create_table "table_points", ;options=>"ENGINE=MyISAM", :force => true do |t| 92 | t.string :data 93 | t.point :geom, :null => false 94 | end 95 | 96 | add_index :table_points, :geom, :spatial => true 97 | end 98 | 99 | === Differences Between Databases 100 | 101 | - On all versions of MySQL, the :srid, :with_z, and :with_m options are ignored, 102 | since they are not supported. 103 | 104 | - On MySQL versions <= 5.0.16, you have to add :options => 105 | "ENGINE=MyISAM" to the create_table statement, since only MyISAM tables 106 | can have spatial columns. In addition, only MyISAM tables may have spatial 107 | indexes. 108 | 109 | === Models 110 | 111 | Create your ActiveRecord models normally. Spatial Adapter will automatically 112 | handle spatial columns, converting them to the appropriate GeoRuby type. 113 | 114 | class TablePoint < ActiveRecord::Base 115 | end 116 | 117 | === Access 118 | 119 | Here is an example of row creation and access, using the model and the table 120 | defined above: 121 | 122 | pt = TablePoint.new( 123 | :data => "Hello!", 124 | :geom => Point.from_x_y_z(-1.6, 2.8, -3.4, 123)) 125 | pt.save 126 | pt = TablePoint.find_first 127 | puts pt.geom.x #access the geom column like any other 128 | 129 | === Fixtures 130 | 131 | If you use fixtures for your unit tests, at some point, you will want to input 132 | a geometry. You could transform your geometries to a form suitable for YAML 133 | yourself every time but Spatial Adapter provides a method to do it for you: 134 | +to_fixture_format+. You would use it like this, if the geometric column is a 135 | point: 136 | 137 | fixture: 138 | id: 1 139 | data: HELLO 140 | geom: <%= Point.from_x_y(123.5,321.9).to_fixture_format %> 141 | 142 | === Finder Enhancements 143 | 144 | Enhancements to find_by_* and friends has been removed from this version of 145 | Spatial Adapter until a cleaner implementation can be made. (The previous 146 | implementation made adapter-specific modifications to ActiveRecord::Base, 147 | which prevented multiple adapters from being loaded at once.) 148 | 149 | === Geometric data types 150 | 151 | Ruby geometric datatypes are currently made available only through the GeoRuby 152 | library (http://georuby.rubyforge.org/): This is where the 153 | Point.from_x_y in the example above comes from. 154 | 155 | == Warning 156 | 157 | - Since ActiveRecord seems to keep only the string values directly returned 158 | from the database, it translates from these to the correct types everytime 159 | an attribute is read, which is probably ok for simple types, but might be 160 | less than efficient for geometries, since the EWKB string has to be parsed 161 | everytime. Also it means you cannot modify the geometry object returned from 162 | an attribute directly: 163 | 164 | place = Place.find_first 165 | place.the_geom.y=123456.7 # this doesn't work 166 | 167 | Since the translation to a geometry is performed every time the_geom is read, 168 | the change to y will not be saved! You would have to do something like this: 169 | 170 | place = Place.find_first 171 | the_geom = place.the_geom 172 | the_geom.y=123456.7 173 | place.the_geom = the_geom 174 | 175 | == License 176 | 177 | The Spatial Adapter for ActiveRecord is released under the MIT license. 178 | 179 | == Latest Changes 180 | 181 | Spatial Adapter has been refactored and is now available as a Ruby gem. The 182 | dependency on Rails has been removed. Unfortunately, the current version is 183 | without some of the previous functionality, until a cleaner implementation is 184 | made. 185 | 186 | The previous release is available on the "legacy" branch. 187 | 188 | === Removed Features in 0.2.0 189 | 190 | - Compatibility with ActiveRecord/Rails older than version 2.2.2 191 | - enhancements to find_by_* for spatial columns 192 | - to_fixture_format extension to the GeoRuby types 193 | 194 | These will hopefully be added back in the near future. 195 | 196 | == Support 197 | 198 | Any questions, enhancement proposals, bug notifications or corrections can be 199 | made via the project page at http://github.com/fragility/spatial_adapter 200 | 201 | == Running Tests 202 | 203 | The gem depdencencies can be installed with `bundle install`. 204 | 205 | You will need to set up an empty database named `spatial_adapter` for each 206 | adapter you want to test. 207 | 208 | Tests are partitioned by adapter and can be run using separate rake task. 209 | 210 | bundle exec rake spec:[adapter] 211 | -------------------------------------------------------------------------------- /spec/postgresql/connection_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spatial_adapter/postgresql' 3 | require 'db/postgis_raw' 4 | require 'models/common' 5 | 6 | describe "Modified PostgreSQLAdapter" do 7 | before :each do 8 | postgis_connection 9 | @connection = ActiveRecord::Base.connection 10 | end 11 | 12 | describe '#postgis_version' do 13 | it 'should report a version number if PostGIS is installed' do 14 | @connection.should_receive(:select_value).with('SELECT postgis_full_version()').and_return('POSTGIS="1.5.0" GEOS="3.2.0-CAPI-1.6.0" PROJ="Rel. 4.7.1, 23 September 2009" LIBXML="2.7.6" USE_STATS') 15 | @connection.postgis_version.should_not be_nil 16 | end 17 | 18 | it 'should report nil if PostGIS is not installed' do 19 | @connection.should_receive(:select_value).with('SELECT postgis_full_version()').and_raise(ActiveRecord::StatementInvalid) 20 | @connection.postgis_version.should be_nil 21 | end 22 | end 23 | 24 | describe '#postgis_major_version' do 25 | it 'should be the first component of the version number' do 26 | @connection.stub!(:postgis_version).and_return('1.5.0') 27 | @connection.postgis_major_version.should == 1 28 | end 29 | 30 | it 'should be nil if PostGIS is not installed' do 31 | @connection.stub!(:postgis_version).and_return(nil) 32 | @connection.postgis_major_version.should be_nil 33 | end 34 | end 35 | 36 | describe '#postgis_minor_version' do 37 | it 'should be the second component of the version number' do 38 | @connection.stub!(:postgis_version).and_return('1.5.0') 39 | @connection.postgis_minor_version.should == 5 40 | end 41 | 42 | it 'should be nil if PostGIS is not installed' do 43 | @connection.stub!(:postgis_version).and_return(nil) 44 | @connection.postgis_minor_version.should be_nil 45 | end 46 | end 47 | 48 | describe '#spatial?' do 49 | it 'should be true if PostGIS is installed' do 50 | @connection.should_receive(:select_value).with('SELECT postgis_full_version()').and_return('POSTGIS="1.5.0" GEOS="3.2.0-CAPI-1.6.0" PROJ="Rel. 4.7.1, 23 September 2009" LIBXML="2.7.6" USE_STATS') 51 | @connection.should be_spatial 52 | end 53 | 54 | it 'should be false if PostGIS is not installed' do 55 | @connection.should_receive(:select_value).with('SELECT postgis_full_version()').and_raise(ActiveRecord::StatementInvalid) 56 | @connection.should_not be_spatial 57 | end 58 | end 59 | 60 | describe '#supports_geographic?' do 61 | it "should be true for PostGIS version 1.5.0" do 62 | @connection.stub!(:postgis_version).and_return('1.5.0') 63 | @connection.supports_geographic?.should == true 64 | end 65 | 66 | it "should be true for PostGIS newer than 1.5.0" do 67 | @connection.stub!(:postgis_version).and_return('1.5.1') 68 | @connection.supports_geographic?.should == true 69 | end 70 | 71 | it "should be true for PostGIS older than 1.5.0" do 72 | @connection.stub!(:postgis_version).and_return('1.4.0') 73 | @connection.supports_geographic?.should == false 74 | end 75 | end 76 | 77 | describe "#columns" do 78 | describe "type" do 79 | it "should be a regular SpatialPostgreSQLColumn if column is a geometry data type" do 80 | column = PointModel.columns.select{|c| c.name == 'geom'}.first 81 | column.should be_a(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 82 | column.geometry_type.should == :point 83 | column.should_not be_geographic 84 | end 85 | 86 | it "should be a geographic SpatialPostgreSQLColumn if column is a geography data type" do 87 | column = GeographyPointModel.columns.select{|c| c.name == 'geom'}.first 88 | column.should be_a(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 89 | column.geometry_type.should == :point 90 | column.should be_geographic 91 | end 92 | 93 | it "should be PostgreSQLColumn if column is not a spatial data type" do 94 | PointModel.columns.select{|c| c.name == 'extra'}.first.should be_a(ActiveRecord::ConnectionAdapters::PostgreSQLColumn) 95 | end 96 | end 97 | 98 | describe "@geometry_type" do 99 | it "should be :point for geometry columns restricted to POINT types" do 100 | PointModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :point 101 | end 102 | 103 | it "should be :line_string for geometry columns restricted to LINESTRING types" do 104 | LineStringModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :line_string 105 | end 106 | 107 | it "should be :polygon for geometry columns restricted to POLYGON types" do 108 | PolygonModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :polygon 109 | end 110 | 111 | it "should be :multi_point for geometry columns restricted to MULTIPOINT types" do 112 | MultiPointModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_point 113 | end 114 | 115 | it "should be :multi_line_string for geometry columns restricted to MULTILINESTRING types" do 116 | MultiLineStringModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_line_string 117 | end 118 | 119 | it "should be :multi_polygon for geometry columns restricted to MULTIPOLYGON types" do 120 | MultiPolygonModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_polygon 121 | end 122 | 123 | it "should be :geometry_collection for geometry columns restricted to GEOMETRYCOLLECTION types" do 124 | GeometryCollectionModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :geometry_collection 125 | end 126 | 127 | it "should be :geometry for geometry columns not restricted to a type" do 128 | GeometryModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :geometry 129 | end 130 | 131 | it "should be :point for geography columns restricted to POINT types" do 132 | GeographyPointModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :point 133 | end 134 | 135 | it "should be :line_string for geography columns restricted to LINESTRING types" do 136 | GeographyLineStringModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :line_string 137 | end 138 | 139 | it "should be :polygon for geography columns restricted to POLYGON types" do 140 | GeographyPolygonModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :polygon 141 | end 142 | 143 | it "should be :multi_point for geography columns restricted to MULTIPOINT types" do 144 | GeographyMultiPointModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_point 145 | end 146 | 147 | it "should be :multi_line_string for geography columns restricted to MULTILINESTRING types" do 148 | GeographyMultiLineStringModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_line_string 149 | end 150 | 151 | it "should be :multi_polygon for geography columns restricted to MULTIPOLYGON types" do 152 | GeographyMultiPolygonModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :multi_polygon 153 | end 154 | 155 | it "should be :geometry_collection for geography columns restricted to GEOMETRYCOLLECTION types" do 156 | GeographyGeometryCollectionModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :geometry_collection 157 | end 158 | 159 | it "should be :geometry for geography columns not restricted to a type" do 160 | GeographyModel.columns.select{|c| c.name == 'geom'}.first.geometry_type.should == :geometry 161 | end 162 | end 163 | end 164 | 165 | describe "#indexes" do 166 | before :each do 167 | @indexes = @connection.indexes('point_models') 168 | end 169 | 170 | it "should return an IndexDefinition for each index on the table" do 171 | @indexes.should have(2).items 172 | @indexes.each do |i| 173 | i.should be_a(ActiveRecord::ConnectionAdapters::IndexDefinition) 174 | end 175 | end 176 | 177 | it "should indicate the correct columns in the index" do 178 | @indexes.select{|i| i.name == 'index_point_models_on_geom'}.first.columns.should == ['geom'] 179 | @indexes.select{|i| i.name == 'index_point_models_on_extra'}.first.columns.should == ['extra', 'more_extra'] 180 | end 181 | 182 | it "should be marked as spatial if a GiST index on a geometry column" do 183 | @indexes.select{|i| i.name == 'index_point_models_on_geom'}.first.spatial.should == true 184 | end 185 | 186 | it "should be marked as spatial if a GiST index on a geography column" do 187 | @indexes = @connection.indexes('geography_point_models') 188 | @indexes.select{|i| i.name == 'index_geography_point_models_on_geom'}.first.spatial.should == true 189 | end 190 | 191 | it "should not be marked as spatial if not a GiST index" do 192 | @indexes.select{|i| i.name == 'index_point_models_on_extra'}.first.spatial.should == false 193 | end 194 | 195 | it "should not be marked as spatial if a GiST index on a non-geometry column" do 196 | @connection.execute(<<-SQL) 197 | create table non_spatial_models 198 | ( 199 | id serial primary key, 200 | location point, 201 | extra varchar(255) 202 | ); 203 | create index index_non_spatial_models_on_location on non_spatial_models using gist (box(location, location)); 204 | SQL 205 | @indexes = @connection.indexes('non_spatial_models') 206 | @indexes.select{|i| i.name == 'index_non_spatial_models_on_location'}.first.spatial.should == false 207 | @connection.execute 'drop table non_spatial_models' 208 | end 209 | end 210 | 211 | describe "#add_index" do 212 | it "should create a spatial index given :spatial => true" do 213 | @connection.should_receive(:execute).with(/using gist/i) 214 | @connection.add_index('geometry_models', 'geom', :spatial => true) 215 | end 216 | 217 | it "should not create a spatial index unless specified" do 218 | @connection.should_not_receive(:execute).with(/using gist/i) 219 | @connection.add_index('geometry_models', 'extra') 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /spec/postgresql/models_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared/common_model_actions_spec' 3 | require 'spatial_adapter/postgresql' 4 | require 'db/postgis_raw' 5 | require 'models/common' 6 | 7 | describe "Spatially-enabled Models" do 8 | before :each do 9 | postgis_connection 10 | @connection = ActiveRecord::Base.connection 11 | end 12 | 13 | it_should_behave_like 'common model actions' 14 | 15 | describe "inserting records" do 16 | it 'should save Point objects' do 17 | model = PointModel.new(:extra => 'test', :geom => GeometryFactory.point) 18 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point.as_hex_ewkb)) 19 | model.save.should == true 20 | end 21 | 22 | it 'should save LineString objects' do 23 | model = LineStringModel.new(:extra => 'test', :geom => GeometryFactory.line_string) 24 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.line_string.as_hex_ewkb)) 25 | model.save.should == true 26 | end 27 | 28 | it 'should save Polygon objects' do 29 | model = PolygonModel.new(:extra => 'test', :geom => GeometryFactory.polygon) 30 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.polygon.as_hex_ewkb)) 31 | model.save.should == true 32 | end 33 | 34 | it 'should save MultiPoint objects' do 35 | model = MultiPointModel.new(:extra => 'test', :geom => GeometryFactory.multi_point) 36 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_point.as_hex_ewkb)) 37 | model.save.should == true 38 | end 39 | 40 | it 'should save MultiLineString objects' do 41 | model = MultiLineStringModel.new(:extra => 'test', :geom => GeometryFactory.multi_line_string) 42 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_line_string.as_hex_ewkb)) 43 | model.save.should == true 44 | end 45 | 46 | it 'should save MultiPolygon objects' do 47 | model = MultiPolygonModel.new(:extra => 'test', :geom => GeometryFactory.multi_polygon) 48 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_polygon.as_hex_ewkb)) 49 | model.save.should == true 50 | end 51 | 52 | it 'should save GeometryCollection objects' do 53 | model = GeometryCollectionModel.new(:extra => 'test', :geom => GeometryFactory.geometry_collection) 54 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.geometry_collection.as_hex_ewkb)) 55 | model.save.should == true 56 | end 57 | 58 | it 'should save Geometry objects' do 59 | model = GeometryModel.new(:extra => 'test', :geom => GeometryFactory.point) 60 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point.as_hex_ewkb)) 61 | model.save.should == true 62 | end 63 | 64 | it 'should save 3D Point (with Z coord) objects' do 65 | model = PointzModel.new(:extra => 'test', :geom => GeometryFactory.pointz) 66 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.pointz.as_hex_ewkb)) 67 | model.save.should == true 68 | end 69 | 70 | it 'should save 3D Point (with M coord) objects' do 71 | model = PointmModel.new(:extra => 'test', :geom => GeometryFactory.pointm) 72 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.pointm.as_hex_ewkb)) 73 | model.save.should == true 74 | end 75 | 76 | it 'should save 4D Point objects' do 77 | model = Point4Model.new(:extra => 'test', :geom => GeometryFactory.point4) 78 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point4.as_hex_ewkb)) 79 | model.save.should == true 80 | end 81 | 82 | it 'should save Point geography objects' do 83 | model = GeographyPointModel.new(:extra => 'test', :geom => GeometryFactory.point) 84 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point.as_hex_ewkb)) 85 | model.save.should == true 86 | end 87 | 88 | it 'should save LineString geography objects' do 89 | model = GeographyLineStringModel.new(:extra => 'test', :geom => GeometryFactory.line_string) 90 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.line_string.as_hex_ewkb)) 91 | model.save.should == true 92 | end 93 | 94 | it 'should save Polygon geography objects' do 95 | model = GeographyPolygonModel.new(:extra => 'test', :geom => GeometryFactory.polygon) 96 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.polygon.as_hex_ewkb)) 97 | model.save.should == true 98 | end 99 | 100 | it 'should save MultiPoint geography objects' do 101 | model = GeographyMultiPointModel.new(:extra => 'test', :geom => GeometryFactory.multi_point) 102 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_point.as_hex_ewkb)) 103 | model.save.should == true 104 | end 105 | 106 | it 'should save MultiLineString geography objects' do 107 | model = GeographyMultiLineStringModel.new(:extra => 'test', :geom => GeometryFactory.multi_line_string) 108 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_line_string.as_hex_ewkb)) 109 | model.save.should == true 110 | end 111 | 112 | it 'should save MultiPolygon geography objects' do 113 | model = GeographyMultiPolygonModel.new(:extra => 'test', :geom => GeometryFactory.multi_polygon) 114 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.multi_polygon.as_hex_ewkb)) 115 | model.save.should == true 116 | end 117 | 118 | it 'should save GeometryCollection geography objects' do 119 | model = GeographyGeometryCollectionModel.new(:extra => 'test', :geom => GeometryFactory.geometry_collection) 120 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.geometry_collection.as_hex_ewkb)) 121 | model.save.should == true 122 | end 123 | 124 | it 'should save Geography objects' do 125 | model = GeographyModel.new(:extra => 'test', :geom => GeometryFactory.point) 126 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point.as_hex_ewkb)) 127 | model.save.should == true 128 | end 129 | 130 | it 'should save 3D Point (with Z coord) geography objects' do 131 | model = GeographyPointzModel.new(:extra => 'test', :geom => GeometryFactory.pointz) 132 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.pointz.as_hex_ewkb)) 133 | model.save.should == true 134 | end 135 | 136 | it 'should save 3D Point (with M coord) geography objects' do 137 | model = GeographyPointmModel.new(:extra => 'test', :geom => GeometryFactory.pointm) 138 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.pointm.as_hex_ewkb)) 139 | model.save.should == true 140 | end 141 | 142 | it 'should save 4D Point geography objects' do 143 | model = GeographyPoint4Model.new(:extra => 'test', :geom => GeometryFactory.point4) 144 | @connection.should_receive(:select_value).with(Regexp.new(GeometryFactory.point4.as_hex_ewkb)) 145 | model.save.should == true 146 | end 147 | end 148 | 149 | describe "finding records" do 150 | it 'should retrieve 3D Point (with Z coord) objects' do 151 | model = PointzModel.create(:extra => 'test', :geom => GeometryFactory.pointz) 152 | PointzModel.find(model.id).geom.should == GeometryFactory.pointz 153 | end 154 | 155 | it 'should retrieve 3D Point (with M coord) objects' do 156 | model = GeographyPointmModel.create(:extra => 'test', :geom => GeometryFactory.pointm) 157 | GeographyPointmModel.find(model.id).geom.should == GeometryFactory.pointm 158 | end 159 | 160 | it 'should retrieve 4D Point objects' do 161 | model = GeographyPoint4Model.create(:extra => 'test', :geom => GeometryFactory.point4) 162 | GeographyPoint4Model.find(model.id).geom.should == GeometryFactory.point4 163 | end 164 | 165 | it 'should retrieve Point geography objects' do 166 | model = GeographyPointModel.create(:extra => 'test', :geom => GeometryFactory.point) 167 | GeographyPointModel.find(model.id).geom.should == GeometryFactory.point 168 | end 169 | 170 | it 'should retrieve LineString geography objects' do 171 | model = GeographyLineStringModel.create(:extra => 'test', :geom => GeometryFactory.line_string) 172 | GeographyLineStringModel.find(model.id).geom.should == GeometryFactory.line_string 173 | end 174 | 175 | it 'should retrieve Polygon geography objects' do 176 | model = GeographyPolygonModel.create(:extra => 'test', :geom => GeometryFactory.polygon) 177 | GeographyPolygonModel.find(model.id).geom.should == GeometryFactory.polygon 178 | end 179 | 180 | it 'should retrieve MultiPoint geography objects' do 181 | model = GeographyMultiPointModel.create(:extra => 'test', :geom => GeometryFactory.multi_point) 182 | GeographyMultiPointModel.find(model.id).geom.should == GeometryFactory.multi_point 183 | end 184 | 185 | it 'should retrieve MultiLineString geography objects' do 186 | model = GeographyMultiLineStringModel.create(:extra => 'test', :geom => GeometryFactory.multi_line_string) 187 | GeographyMultiLineStringModel.find(model.id).geom.should == GeometryFactory.multi_line_string 188 | end 189 | 190 | it 'should retrieve MultiPolygon geography objects' do 191 | model = GeographyMultiPolygonModel.create(:extra => 'test', :geom => GeometryFactory.multi_polygon) 192 | GeographyMultiPolygonModel.find(model.id).geom.should == GeometryFactory.multi_polygon 193 | end 194 | 195 | it 'should retrieve GeometryCollection geography objects' do 196 | model = GeographyGeometryCollectionModel.create(:extra => 'test', :geom => GeometryFactory.geometry_collection) 197 | GeographyGeometryCollectionModel.find(model.id).geom.should == GeometryFactory.geometry_collection 198 | end 199 | 200 | it 'should retrieve Geometry geography objects' do 201 | model = GeographyModel.create(:extra => 'test', :geom => GeometryFactory.point) 202 | GeographyModel.find(model.id).geom.should == GeometryFactory.point 203 | end 204 | 205 | it 'should retrieve 3D Point (with Z coord) geography objects' do 206 | model = GeographyPointzModel.create(:extra => 'test', :geom => GeometryFactory.pointz) 207 | GeographyPointzModel.find(model.id).geom.should == GeometryFactory.pointz 208 | end 209 | 210 | it 'should retrieve 3D Point (with M coord) geography objects' do 211 | model = GeographyPointmModel.create(:extra => 'test', :geom => GeometryFactory.pointm) 212 | GeographyPointmModel.find(model.id).geom.should == GeometryFactory.pointm 213 | end 214 | 215 | it 'should retrieve 4D Point geography objects' do 216 | model = GeographyPoint4Model.create(:extra => 'test', :geom => GeometryFactory.point4) 217 | GeographyPoint4Model.find(model.id).geom.should == GeometryFactory.point4 218 | end 219 | end 220 | end 221 | 222 | -------------------------------------------------------------------------------- /spec/postgresql/migration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'spatial_adapter/postgresql' 3 | 4 | class MigratedGeometryModel < ActiveRecord::Base 5 | end 6 | 7 | describe "Spatially-enabled Migrations" do 8 | before :each do 9 | postgis_connection 10 | @connection = ActiveRecord::Base.connection 11 | end 12 | 13 | describe "creating tables" do 14 | after :each do 15 | @connection.drop_table "migrated_geometry_models" 16 | end 17 | 18 | SpatialAdapter.geometry_data_types.keys.each do |type| 19 | it "should create #{type.to_s} columns" do 20 | ActiveRecord::Schema.define do 21 | create_table :migrated_geometry_models, :force => true do |t| 22 | t.integer :extra 23 | t.send(type, :geom) 24 | end 25 | end 26 | 27 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 28 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 29 | geom_column.type.should == :string 30 | geom_column.geometry_type.should == type 31 | geom_column.should_not be_geographic 32 | geom_column.with_z.should == false 33 | geom_column.with_m.should == false 34 | geom_column.srid.should == -1 35 | end 36 | 37 | it "should create #{type.to_s} geographic columns" do 38 | ActiveRecord::Schema.define do 39 | create_table :migrated_geometry_models, :force => true do |t| 40 | t.integer :extra 41 | t.column :geom, type, :geographic => true 42 | end 43 | end 44 | 45 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 46 | 47 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 48 | geom_column.type.should == :string 49 | geom_column.geometry_type.should == type 50 | geom_column.should be_geographic 51 | geom_column.with_z.should == false 52 | geom_column.with_m.should == false 53 | #geom_column.srid.should == 4326 # SRID is currently irrelevant for geography columns 54 | end 55 | end 56 | 57 | 58 | it "should create 3d (xyz) geometry columns" do 59 | ActiveRecord::Schema.define do 60 | create_table :migrated_geometry_models, :force => true do |t| 61 | t.integer :extra 62 | t.point :geom, :with_z => true 63 | end 64 | end 65 | 66 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 67 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 68 | geom_column.with_z.should == true 69 | geom_column.with_m.should == false 70 | geom_column.srid.should == -1 71 | end 72 | 73 | 74 | it "should create 3d (xym) geometry columns" do 75 | ActiveRecord::Schema.define do 76 | create_table :migrated_geometry_models, :force => true do |t| 77 | t.integer :extra 78 | t.point :geom, :with_m => true 79 | end 80 | end 81 | 82 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 83 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 84 | geom_column.geometry_type.should == :point 85 | geom_column.type.should == :string 86 | geom_column.with_z.should == false 87 | geom_column.with_m.should == true 88 | geom_column.srid.should == -1 89 | end 90 | 91 | 92 | it "should create 4d (xyzm) geometry columns" do 93 | ActiveRecord::Schema.define do 94 | create_table :migrated_geometry_models, :force => true do |t| 95 | t.integer :extra 96 | t.point :geom, :with_z => true, :with_m => true 97 | end 98 | end 99 | 100 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 101 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 102 | geom_column.geometry_type.should == :point 103 | geom_column.type.should == :string 104 | geom_column.with_z.should == true 105 | geom_column.with_m.should == true 106 | geom_column.srid.should == -1 107 | end 108 | 109 | it "should create 3d (xyz) geographic columns" do 110 | ActiveRecord::Schema.define do 111 | create_table :migrated_geometry_models, :force => true do |t| 112 | t.integer :extra 113 | t.point :geom, :with_z => true, :geographic => true 114 | end 115 | end 116 | 117 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 118 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 119 | geom_column.should be_geographic 120 | geom_column.with_z.should == true 121 | geom_column.with_m.should == false 122 | end 123 | 124 | 125 | it "should create 3d (xym) geographic columns" do 126 | ActiveRecord::Schema.define do 127 | create_table :migrated_geometry_models, :force => true do |t| 128 | t.integer :extra 129 | t.point :geom, :with_m => true, :geographic => true 130 | end 131 | end 132 | 133 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 134 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 135 | geom_column.geometry_type.should == :point 136 | geom_column.type.should == :string 137 | geom_column.should be_geographic 138 | geom_column.with_z.should == false 139 | geom_column.with_m.should == true 140 | end 141 | 142 | 143 | it "should create 4d (xyzm) geographic columns" do 144 | ActiveRecord::Schema.define do 145 | create_table :migrated_geometry_models, :force => true do |t| 146 | t.integer :extra 147 | t.point :geom, :with_z => true, :with_m => true, :geographic => true 148 | end 149 | end 150 | 151 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 152 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 153 | geom_column.geometry_type.should == :point 154 | geom_column.should be_geographic 155 | geom_column.type.should == :string 156 | geom_column.with_z.should == true 157 | geom_column.with_m.should == true 158 | end 159 | 160 | 161 | it "should create geometry columns with specified SRID" do 162 | ActiveRecord::Schema.define do 163 | create_table :migrated_geometry_models, :force => true do |t| 164 | t.integer :extra 165 | t.geometry :geom, :srid => 4326 166 | end 167 | end 168 | 169 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 170 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 171 | geom_column.type.should == :string 172 | geom_column.geometry_type.should == :geometry 173 | geom_column.with_z.should == false 174 | geom_column.with_m.should == false 175 | geom_column.srid.should == 4326 176 | end 177 | end 178 | 179 | describe "adding columns" do 180 | before :each do 181 | ActiveRecord::Schema.define do 182 | create_table :migrated_geometry_models, :force => true do |t| 183 | t.integer :extra 184 | end 185 | end 186 | end 187 | 188 | after :each do 189 | @connection.drop_table "migrated_geometry_models" 190 | end 191 | 192 | SpatialAdapter.geometry_data_types.keys.each do |type| 193 | it "should add #{type.to_s} columns" do 194 | ActiveRecord::Schema.define do 195 | add_column :migrated_geometry_models, :geom, type 196 | end 197 | 198 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 199 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 200 | geom_column.type.should == :string 201 | geom_column.geometry_type.should == type 202 | geom_column.with_z.should == false 203 | geom_column.with_m.should == false 204 | geom_column.srid.should == -1 205 | end 206 | end 207 | 208 | it "should add 3d (xyz) geometry columns" do 209 | ActiveRecord::Schema.define do 210 | add_column :migrated_geometry_models, :geom, :point, :with_z => true 211 | end 212 | 213 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 214 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 215 | geom_column.type.should == :string 216 | geom_column.geometry_type.should == :point 217 | geom_column.with_z.should == true 218 | geom_column.with_m.should == false 219 | geom_column.srid.should == -1 220 | end 221 | 222 | 223 | it "should add 3d (xym) geometry columns" do 224 | ActiveRecord::Schema.define do 225 | add_column :migrated_geometry_models, :geom, :point, :with_m => true 226 | end 227 | 228 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 229 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 230 | geom_column.type.should == :string 231 | geom_column.geometry_type.should == :point 232 | geom_column.with_z.should == false 233 | geom_column.with_m.should == true 234 | geom_column.srid.should == -1 235 | end 236 | 237 | 238 | it "should add 4d (xyzm) geometry columns" do 239 | ActiveRecord::Schema.define do 240 | add_column :migrated_geometry_models, :geom, :point, :with_z => true, :with_m => true 241 | end 242 | 243 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 244 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 245 | geom_column.type.should == :string 246 | geom_column.geometry_type.should == :point 247 | geom_column.with_z.should == true 248 | geom_column.with_m.should == true 249 | geom_column.srid.should == -1 250 | end 251 | 252 | it "should add 3d (xyz) geography columns" do 253 | ActiveRecord::Schema.define do 254 | add_column :migrated_geometry_models, :geom, :point, :with_z => true, :geographic => true 255 | end 256 | 257 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 258 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 259 | geom_column.type.should == :string 260 | geom_column.should be_geographic 261 | geom_column.geometry_type.should == :point 262 | geom_column.with_z.should == true 263 | geom_column.with_m.should == false 264 | end 265 | 266 | 267 | it "should add 3d (xym) geography columns" do 268 | ActiveRecord::Schema.define do 269 | add_column :migrated_geometry_models, :geom, :point, :with_m => true, :geographic => true 270 | end 271 | 272 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 273 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 274 | geom_column.type.should == :string 275 | geom_column.should be_geographic 276 | geom_column.geometry_type.should == :point 277 | geom_column.with_z.should == false 278 | geom_column.with_m.should == true 279 | end 280 | 281 | 282 | it "should add 4d (xyzm) geography columns" do 283 | ActiveRecord::Schema.define do 284 | add_column :migrated_geometry_models, :geom, :point, :with_z => true, :with_m => true, :geographic => true 285 | end 286 | 287 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 288 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 289 | geom_column.type.should == :string 290 | geom_column.should be_geographic 291 | geom_column.geometry_type.should == :point 292 | geom_column.with_z.should == true 293 | geom_column.with_m.should == true 294 | end 295 | 296 | it "should add GEOMETRY columns with specified SRID" do 297 | ActiveRecord::Schema.define do 298 | add_column :migrated_geometry_models, :geom, :geometry, :srid => 4326 299 | end 300 | 301 | geom_column = @connection.columns(:migrated_geometry_models).select{|c| c.name == 'geom'}.first 302 | geom_column.should be_a(SpatialAdapter::SpatialColumn) 303 | geom_column.geometry_type.should == :geometry 304 | geom_column.type.should == :string 305 | geom_column.with_z.should == false 306 | geom_column.with_m.should == false 307 | geom_column.srid.should == 4326 308 | end 309 | end 310 | 311 | describe "removing columns" do 312 | after :each do 313 | @connection.drop_table "migrated_geometry_models" 314 | end 315 | 316 | SpatialAdapter.geometry_data_types.keys.each do |type| 317 | it "should remove #{type.to_s} columns using DropGeometryColumn" do 318 | ActiveRecord::Schema.define do 319 | create_table :migrated_geometry_models, :force => true do |t| 320 | t.integer :extra 321 | t.send(type, :geom) 322 | end 323 | end 324 | 325 | @connection.should_receive(:execute).with(/DropGeometryColumn(.*migrated_geometry_models.*geom)/) 326 | ActiveRecord::Schema.define do 327 | remove_column :migrated_geometry_models, :geom 328 | end 329 | @connection.should_receive(:execute).with(anything()) 330 | end 331 | end 332 | 333 | SpatialAdapter.geometry_data_types.keys.each do |type| 334 | it "should remove #{type.to_s} geography columns using ALTER TABLE DROP COLUMN" do 335 | ActiveRecord::Schema.define do 336 | create_table :migrated_geometry_models, :force => true do |t| 337 | t.integer :extra 338 | t.send(type, :geom, :geographic => true) 339 | end 340 | end 341 | 342 | @connection.should_receive(:execute).with(/alter table.*migrated_geometry_models.*drop.*geom/i) 343 | ActiveRecord::Schema.define do 344 | remove_column :migrated_geometry_models, :geom 345 | end 346 | @connection.should_receive(:execute).with(anything()) 347 | end 348 | end 349 | 350 | it "should still remove non-spatial columns using ALTER TABLE DROP COLUMN" do 351 | ActiveRecord::Schema.define do 352 | create_table :migrated_geometry_models, :force => true do |t| 353 | t.integer :extra 354 | t.point :geom 355 | end 356 | end 357 | 358 | @connection.should_receive(:execute).with(/alter table.*migrated_geometry_models.*drop.*extra/i) 359 | ActiveRecord::Schema.define do 360 | remove_column :migrated_geometry_models, :extra 361 | end 362 | @connection.should_receive(:execute).with(anything()) 363 | end 364 | end 365 | end 366 | -------------------------------------------------------------------------------- /lib/spatial_adapter/postgresql.rb: -------------------------------------------------------------------------------- 1 | require 'spatial_adapter' 2 | require 'active_record/connection_adapters/postgresql_adapter' 3 | 4 | module ActiveRecord::ConnectionAdapters 5 | class PostgreSQLAdapter 6 | def postgis_version 7 | begin 8 | select_value("SELECT postgis_full_version()").scan(/POSTGIS="([\d\.]*)"/)[0][0] 9 | rescue ActiveRecord::StatementInvalid 10 | nil 11 | end 12 | end 13 | 14 | def postgis_major_version 15 | version = postgis_version 16 | version ? version.scan(/^(\d)\.\d\.\d$/)[0][0].to_i : nil 17 | end 18 | 19 | def postgis_minor_version 20 | version = postgis_version 21 | version ? version.scan(/^\d\.(\d)\.\d$/)[0][0].to_i : nil 22 | end 23 | 24 | def spatial? 25 | !postgis_version.nil? 26 | end 27 | 28 | def supports_geographic? 29 | postgis_major_version > 1 || (postgis_major_version == 1 && postgis_minor_version >= 5) 30 | end 31 | 32 | alias :original_native_database_types :native_database_types 33 | def native_database_types 34 | original_native_database_types.merge!(SpatialAdapter.geometry_data_types) 35 | end 36 | 37 | alias :original_quote :quote 38 | #Redefines the quote method to add behaviour for when a Geometry is encountered 39 | def quote(value, column = nil) 40 | if value.kind_of?(GeoRuby::SimpleFeatures::Geometry) 41 | "'#{value.as_hex_ewkb}'" 42 | else 43 | original_quote(value,column) 44 | end 45 | end 46 | 47 | def columns(table_name, name = nil) #:nodoc: 48 | raw_geom_infos = column_spatial_info(table_name) 49 | 50 | column_definitions(table_name).collect do |name, type, default, notnull| 51 | case type 52 | when /geography/i 53 | ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_from_geography(name, default, type, notnull == 'f') 54 | when /geometry/i 55 | raw_geom_info = raw_geom_infos[name] 56 | if raw_geom_info.nil? 57 | # This column isn't in the geometry_columns table, so we don't know anything else about it 58 | ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_simplified(name, default, notnull == "f") 59 | else 60 | ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.new(name, default, raw_geom_info.type, notnull == "f", raw_geom_info.srid, raw_geom_info.with_z, raw_geom_info.with_m) 61 | end 62 | else 63 | ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == "f") 64 | end 65 | end 66 | end 67 | 68 | def create_table(table_name, options = {}) 69 | # Using the subclassed table definition 70 | table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self) 71 | table_definition.primary_key(options[:primary_key] || ActiveRecord::Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false 72 | 73 | yield table_definition if block_given? 74 | 75 | if options[:force] && table_exists?(table_name) 76 | drop_table(table_name, options) 77 | end 78 | 79 | create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " 80 | create_sql << "#{quote_table_name(table_name)} (" 81 | create_sql << table_definition.to_sql 82 | create_sql << ") #{options[:options]}" 83 | 84 | # This is the additional portion for PostGIS 85 | unless table_definition.geom_columns.nil? 86 | table_definition.geom_columns.each do |geom_column| 87 | geom_column.table_name = table_name 88 | create_sql << "; " + geom_column.to_sql 89 | end 90 | end 91 | 92 | execute create_sql 93 | end 94 | 95 | alias :original_remove_column :remove_column 96 | def remove_column(table_name, *column_names) 97 | column_names = column_names.flatten 98 | columns(table_name).each do |col| 99 | if column_names.include?(col.name.to_sym) 100 | # Geometry columns have to be removed using DropGeometryColumn 101 | if col.is_a?(::SpatialAdapter::SpatialColumn) && col.spatial? && !col.geographic? 102 | execute "SELECT DropGeometryColumn('#{table_name}','#{col.name}')" 103 | else 104 | original_remove_column(table_name, col.name) 105 | end 106 | end 107 | end 108 | end 109 | 110 | alias :original_add_column :add_column 111 | def add_column(table_name, column_name, type, options = {}) 112 | unless SpatialAdapter.geometry_data_types[type].nil? 113 | geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition.new(self, column_name, type, nil, nil, options[:null], options[:srid] || -1 , options[:with_z] || false , options[:with_m] || false, options[:geographic] || false) 114 | if geom_column.geographic 115 | default = options[:default] 116 | notnull = options[:null] == false 117 | 118 | execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{geom_column.to_sql}") 119 | 120 | change_column_default(table_name, column_name, default) if options_include_default?(options) 121 | change_column_null(table_name, column_name, false, default) if notnull 122 | else 123 | geom_column.table_name = table_name 124 | execute geom_column.to_sql 125 | end 126 | else 127 | original_add_column(table_name, column_name, type, options) 128 | end 129 | end 130 | 131 | # Adds an index to a column. 132 | def add_index(table_name, column_name, options = {}) 133 | column_names = Array(column_name) 134 | index_name = index_name(table_name, :column => column_names) 135 | 136 | if Hash === options # legacy support, since this param was a string 137 | index_type = options[:unique] ? "UNIQUE" : "" 138 | index_name = options[:name] || index_name 139 | index_method = options[:spatial] ? 'USING GIST' : "" 140 | else 141 | index_type = options 142 | end 143 | quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ") 144 | execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_method} (#{quoted_column_names})" 145 | end 146 | 147 | # Returns the list of all indexes for a table. 148 | # 149 | # This is a full replacement for the ActiveRecord method and as a result 150 | # has a higher probability of breaking in future releases. 151 | def indexes(table_name, name = nil) 152 | schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',') 153 | 154 | # Changed from upstread: link to pg_am to grab the index type (e.g. "gist") 155 | result = query(<<-SQL, name) 156 | SELECT distinct i.relname, d.indisunique, d.indkey, t.oid, am.amname 157 | FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am 158 | WHERE i.relkind = 'i' 159 | AND d.indexrelid = i.oid 160 | AND d.indisprimary = 'f' 161 | AND t.oid = d.indrelid 162 | AND t.relname = '#{table_name}' 163 | AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) ) 164 | AND i.relam = am.oid 165 | AND a.attrelid = t.oid 166 | ORDER BY i.relname 167 | SQL 168 | 169 | 170 | indexes = [] 171 | 172 | indexes = result.map do |row| 173 | index_name = row[0] 174 | unique = row[1] == 't' 175 | indkey = row[2].split(" ") 176 | oid = row[3] 177 | indtype = row[4] 178 | 179 | # Changed from upstream: need to get the column types to test for spatial indexes 180 | columns = query(<<-SQL, "Columns for index #{row[0]} on #{table_name}").inject({}) {|attlist, r| attlist[r[1]] = [r[0], r[2]]; attlist} 181 | SELECT a.attname, a.attnum, t.typname 182 | FROM pg_attribute a, pg_type t 183 | WHERE a.attrelid = #{oid} 184 | AND a.attnum IN (#{indkey.join(",")}) 185 | AND a.atttypid = t.oid 186 | SQL 187 | 188 | # Only GiST indexes on spatial columns denote a spatial index 189 | spatial = indtype == 'gist' && columns.size == 1 && (columns.values.first[1] == 'geometry' || columns.values.first[1] == 'geography') 190 | 191 | column_names = indkey.map {|attnum| columns[attnum] ? columns[attnum][0] : nil } 192 | ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, spatial) 193 | end 194 | 195 | indexes 196 | end 197 | 198 | def disable_referential_integrity(&block) #:nodoc: 199 | if supports_disable_referential_integrity?() then 200 | execute(tables_without_postgis.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) 201 | end 202 | yield 203 | ensure 204 | if supports_disable_referential_integrity?() then 205 | execute(tables_without_postgis.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) 206 | end 207 | end 208 | 209 | private 210 | 211 | def tables_without_postgis 212 | tables - %w{ geometry_columns spatial_ref_sys } 213 | end 214 | 215 | def column_spatial_info(table_name) 216 | constr = query("SELECT * FROM geometry_columns WHERE f_table_name = '#{table_name}'") 217 | 218 | raw_geom_infos = {} 219 | constr.each do |constr_def_a| 220 | raw_geom_infos[constr_def_a[3]] ||= SpatialAdapter::RawGeomInfo.new 221 | raw_geom_infos[constr_def_a[3]].type = constr_def_a[6] 222 | raw_geom_infos[constr_def_a[3]].dimension = constr_def_a[4].to_i 223 | raw_geom_infos[constr_def_a[3]].srid = constr_def_a[5].to_i 224 | 225 | if raw_geom_infos[constr_def_a[3]].type[-1] == ?M 226 | raw_geom_infos[constr_def_a[3]].with_m = true 227 | raw_geom_infos[constr_def_a[3]].type.chop! 228 | else 229 | raw_geom_infos[constr_def_a[3]].with_m = false 230 | end 231 | end 232 | 233 | raw_geom_infos.each_value do |raw_geom_info| 234 | #check the presence of z and m 235 | raw_geom_info.convert! 236 | end 237 | 238 | raw_geom_infos 239 | end 240 | end 241 | 242 | class PostgreSQLTableDefinition < TableDefinition 243 | attr_reader :geom_columns 244 | 245 | def column(name, type, options = {}) 246 | unless (SpatialAdapter.geometry_data_types[type.to_sym].nil? or 247 | (options[:create_using_addgeometrycolumn] == false)) 248 | 249 | column = self[name] || PostgreSQLColumnDefinition.new(@base, name, type) 250 | column.null = options[:null] 251 | column.srid = options[:srid] || -1 252 | column.with_z = options[:with_z] || false 253 | column.with_m = options[:with_m] || false 254 | column.geographic = options[:geographic] || false 255 | 256 | if column.geographic 257 | @columns << column unless @columns.include? column 258 | else 259 | # Hold this column for later 260 | @geom_columns ||= [] 261 | @geom_columns << column 262 | end 263 | self 264 | else 265 | super(name, type, options) 266 | end 267 | end 268 | end 269 | 270 | class PostgreSQLColumnDefinition < ColumnDefinition 271 | attr_accessor :table_name 272 | attr_accessor :srid, :with_z, :with_m, :geographic 273 | attr_reader :spatial 274 | 275 | def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil, null=nil, srid=-1, with_z=false, with_m=false, geographic=false) 276 | super(base, name, type, limit, default, null) 277 | @table_name = nil 278 | @spatial = true 279 | @srid = srid 280 | @with_z = with_z 281 | @with_m = with_m 282 | @geographic = geographic 283 | end 284 | 285 | def sql_type 286 | if geographic 287 | type_sql = SpatialAdapter.geometry_data_types[type.to_sym][:name] 288 | type_sql += "Z" if with_z 289 | type_sql += "M" if with_m 290 | # SRID is not yet supported (defaults to 4326) 291 | #type_sql += ", #{srid}" if (srid && srid != -1) 292 | type_sql = "geography(#{type_sql})" 293 | type_sql 294 | else 295 | super 296 | end 297 | end 298 | 299 | def to_sql 300 | if spatial && !geographic 301 | type_sql = SpatialAdapter.geometry_data_types[type.to_sym][:name] 302 | type_sql += "M" if with_m and !with_z 303 | if with_m and with_z 304 | dimension = 4 305 | elsif with_m or with_z 306 | dimension = 3 307 | else 308 | dimension = 2 309 | end 310 | 311 | column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})" 312 | column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false 313 | column_sql 314 | else 315 | super 316 | end 317 | end 318 | end 319 | 320 | class SpatialPostgreSQLColumn < PostgreSQLColumn 321 | include SpatialAdapter::SpatialColumn 322 | 323 | def initialize(name, default, sql_type = nil, null = true, srid=-1, with_z=false, with_m=false, geographic = false) 324 | super(name, default, sql_type, null, srid, with_z, with_m) 325 | @geographic = geographic 326 | end 327 | 328 | def geographic? 329 | @geographic 330 | end 331 | 332 | #Transforms a string to a geometry. PostGIS returns a HewEWKB string. 333 | def self.string_to_geometry(string) 334 | return string unless string.is_a?(String) 335 | GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil 336 | end 337 | 338 | def self.create_simplified(name, default, null = true) 339 | new(name, default, "geometry", null) 340 | end 341 | 342 | def self.create_from_geography(name, default, sql_type, null = true) 343 | params = extract_geography_params(sql_type) 344 | new(name, default, sql_type, null, params[:srid], params[:with_z], params[:with_m], true) 345 | end 346 | 347 | private 348 | 349 | # Add detection of PostGIS-specific geography columns 350 | def geometry_simplified_type(sql_type) 351 | case sql_type 352 | when /geography\(point/i then :point 353 | when /geography\(linestring/i then :line_string 354 | when /geography\(polygon/i then :polygon 355 | when /geography\(multipoint/i then :multi_point 356 | when /geography\(multilinestring/i then :multi_line_string 357 | when /geography\(multipolygon/i then :multi_polygon 358 | when /geography\(geometrycollection/i then :geometry_collection 359 | when /geography/i then :geometry 360 | else 361 | super 362 | end 363 | end 364 | 365 | def self.extract_geography_params(sql_type) 366 | params = { 367 | :srid => 0, 368 | :with_z => false, 369 | :with_m => false 370 | } 371 | if sql_type =~ /geography(?:\((?:\w+?)(Z)?(M)?(?:,(\d+))?\))?/i 372 | params[:with_z] = $1 == 'Z' 373 | params[:with_m] = $2 == 'M' 374 | params[:srid] = $3.to_i 375 | end 376 | params 377 | end 378 | end 379 | end 380 | --------------------------------------------------------------------------------