activerecord-spatial 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1670 -0
  3. data/Gemfile +12 -13
  4. data/Guardfile +7 -10
  5. data/MIT-LICENSE +1 -1
  6. data/README.rdoc +8 -19
  7. data/Rakefile +2 -1
  8. data/activerecord-spatial.gemspec +12 -13
  9. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions/active_record.rb +46 -0
  10. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +7 -38
  11. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/postgis.rb +6 -7
  12. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/unknown_srid.rb +4 -5
  13. data/lib/activerecord-spatial/active_record/models/geography_column.rb +1 -2
  14. data/lib/activerecord-spatial/active_record/models/geometry_column.rb +1 -2
  15. data/lib/activerecord-spatial/active_record/models/spatial_column.rb +1 -2
  16. data/lib/activerecord-spatial/active_record/models/spatial_ref_sys.rb +5 -6
  17. data/lib/activerecord-spatial/active_record.rb +0 -1
  18. data/lib/activerecord-spatial/associations/active_record.rb +62 -120
  19. data/lib/activerecord-spatial/associations/base.rb +26 -75
  20. data/lib/activerecord-spatial/associations/preloader/spatial_association.rb +57 -0
  21. data/lib/activerecord-spatial/associations/reflection/spatial_reflection.rb +41 -0
  22. data/lib/activerecord-spatial/associations.rb +26 -4
  23. data/lib/activerecord-spatial/spatial_columns.rb +85 -94
  24. data/lib/activerecord-spatial/spatial_function.rb +62 -51
  25. data/lib/activerecord-spatial/spatial_scope_constants/postgis_2_0.rb +48 -0
  26. data/lib/activerecord-spatial/spatial_scope_constants/postgis_2_2.rb +46 -0
  27. data/lib/activerecord-spatial/spatial_scope_constants/postgis_legacy.rb +30 -0
  28. data/lib/activerecord-spatial/spatial_scope_constants.rb +10 -61
  29. data/lib/activerecord-spatial/spatial_scopes.rb +47 -49
  30. data/lib/activerecord-spatial/version.rb +1 -2
  31. data/lib/activerecord-spatial.rb +2 -6
  32. data/lib/tasks/test.rake +21 -19
  33. data/test/.rubocop.yml +35 -0
  34. data/test/accessors_geographies_tests.rb +19 -19
  35. data/test/accessors_geometries_tests.rb +19 -19
  36. data/test/adapter_tests.rb +1 -2
  37. data/test/associations_tests.rb +181 -203
  38. data/test/geography_column_tests.rb +2 -3
  39. data/test/geometry_column_tests.rb +1 -2
  40. data/test/models/bar.rb +2 -3
  41. data/test/models/blort.rb +1 -2
  42. data/test/models/foo.rb +2 -3
  43. data/test/models/foo3d.rb +2 -3
  44. data/test/models/foo_geography.rb +2 -3
  45. data/test/models/zortable.rb +2 -3
  46. data/test/spatial_function_tests.rb +12 -17
  47. data/test/spatial_scopes_geographies_tests.rb +17 -20
  48. data/test/spatial_scopes_tests.rb +84 -75
  49. data/test/test_helper.rb +66 -79
  50. metadata +16 -14
  51. data/lib/activerecord-spatial/associations/active_record_3.rb +0 -123
@@ -1,21 +1,6 @@
1
1
 
2
2
  module ActiveRecord
3
3
  module Associations #:nodoc:
4
- class SpatialAssociation < HasManyAssociation
5
- attr_reader :geom, :foreign_geom, :relationship, :scope_options
6
-
7
- def initialize(*args)
8
- super
9
-
10
- @geom = self.options[:geom]
11
- @foreign_geom = self.options[:foreign_geom]
12
- @relationship = self.options[:relationship].to_s
13
- @scope_options = (self.options[:scope_options] || {}).merge({
14
- :column => @foreign_geom
15
- })
16
- end
17
- end
18
-
19
4
  class Builder::Spatial < Builder::HasMany #:nodoc:
20
5
  SPATIAL_MACRO = :has_many
21
6
 
@@ -28,65 +13,31 @@ module ActiveRecord
28
13
  :inverse_of
29
14
  ].freeze
30
15
 
31
- private
32
- def dependency_method_name
33
- "spatially_#{self.relationship}_dependent_for_#{name}"
34
- end
16
+ def macro
17
+ SPATIAL_MACRO
18
+ end
19
+
20
+ def self.valid_options(options)
21
+ super + VALID_SPATIAL_OPTIONS - INVALID_SPATIAL_OPTIONS
22
+ end
35
23
  end
36
24
 
37
25
  class Preloader #:nodoc:
38
26
  class SpatialAssociation < HasMany #:nodoc:
39
- SPATIAL_FIELD_ALIAS = '__spatial_ids__'
40
- SPATIAL_JOIN_NAME = '__spatial_ids_join__'
41
- SPATIAL_JOIN_QUOTED_NAME = %{"#{SPATIAL_JOIN_NAME}"}
42
-
43
- private
44
- def associated_records_by_owner
45
- owners_map = owners_by_key
46
- owner_keys = owners_map.keys.compact
47
-
48
- if klass.nil? || owner_keys.empty?
49
- records = []
50
- else
51
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
52
- records = sliced.map { |slice| records_for(slice) }.flatten
53
- end
54
-
55
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
56
-
57
- records.each do |record|
58
- record[SPATIAL_FIELD_ALIAS].split(',').each do |owner_key|
59
- owners_map[owner_key].each do |owner|
60
- records_by_owner[owner] << record
61
- end
62
- end
63
- end
64
-
65
- records_by_owner
66
- end
67
- end
27
+ SPATIAL_FIELD_ALIAS = '__spatial_ids__'.freeze
28
+ SPATIAL_JOIN_NAME = '__spatial_ids_join__'.freeze
29
+ SPATIAL_JOIN_QUOTED_NAME = %{"#{SPATIAL_JOIN_NAME}"}.freeze
30
+ end
68
31
 
69
- def preloader_for_with_spatial(reflection)
70
- if reflection.options[:relationship]
32
+ prepend(Module.new do
33
+ def preloader_for(reflection, *args)
34
+ if reflection.is_a?(ActiveRecord::Reflection::SpatialReflection)
71
35
  SpatialAssociation
72
36
  else
73
- preloader_for_without_spatial(reflection)
37
+ super
74
38
  end
75
39
  end
76
- alias_method_chain :preloader_for, :spatial
77
- end
78
- end
79
-
80
- module Reflection #:nodoc:
81
- class AssociationReflection < MacroReflection #:nodoc:
82
- def association_class_with_spatial
83
- if self.options[:relationship]
84
- Associations::SpatialAssociation
85
- else
86
- association_class_without_spatial
87
- end
88
- end
89
- alias_method_chain :association_class, :spatial
40
+ end)
90
41
  end
91
42
  end
92
43
  end
@@ -98,12 +49,13 @@ end
98
49
  #
99
50
  # class Neighbourhood < ActiveRecord::Base
100
51
  # has_many_spatially :cities,
101
- # :relationship => :contains
52
+ # relationship: :contains
102
53
  # end
103
54
  #
104
55
  # class City < ActiveRecord::Base
105
- # has_many_spatially :neighbourhoods,
106
- # :relationship => :within
56
+ # has_many_spatially :neighbourhoods, -> {
57
+ # where('canonical = true')
58
+ # }, relationship: :within
107
59
  # end
108
60
  #
109
61
  # Neighbourhood.first.cities
@@ -138,7 +90,7 @@ end
138
90
  # table. The default here is again +:the_geom+.
139
91
  # * +:scope_options+ - these are options passed directly to the SpatialScopes
140
92
  # module and as such the options are the same as are available there. The
141
- # default value here is <tt>{ :invert => true }</tt>, as we want our
93
+ # default value here is <tt>{ invert: true }</tt>, as we want our
142
94
  # spatial relationships to say "Foo spatially contains many Bars" and
143
95
  # therefore the relationship in SQL becomes
144
96
  # <tt>ST_contains("foos"."the_geom", "bars"."the_geom")</tt>.
@@ -156,11 +108,11 @@ module ActiveRecordSpatial::Associations
156
108
  extend ActiveSupport::Concern
157
109
 
158
110
  DEFAULT_OPTIONS = {
159
- :relationship => :intersects,
160
- :geom => ActiveRecordSpatial.default_column_name,
161
- :foreign_geom => ActiveRecordSpatial.default_column_name,
162
- :scope_options => {
163
- :invert => true
111
+ relationship: :intersects,
112
+ geom: ActiveRecordSpatial.default_column_name,
113
+ foreign_geom: ActiveRecordSpatial.default_column_name,
114
+ scope_options: {
115
+ invert: true
164
116
  }
165
117
  }.freeze
166
118
 
@@ -179,4 +131,3 @@ module ActiveRecordSpatial::Associations
179
131
  private :build_options
180
132
  end
181
133
  end
182
-
@@ -0,0 +1,57 @@
1
+
2
+ module ActiveRecord
3
+ module Associations
4
+ class Preloader #:nodoc:
5
+ class SpatialAssociation < HasMany #:nodoc:
6
+ if method_defined?(:query_scope)
7
+ def query_scope(ids)
8
+ spatial_query_scope(ids)
9
+ end
10
+ else
11
+ def records_for(ids)
12
+ spatial_query_scope(ids)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def association_key_name
19
+ SPATIAL_FIELD_ALIAS
20
+ end
21
+
22
+ def spatial_query_scope(ids)
23
+ join_name = model.quoted_table_name
24
+ column = %{#{SPATIAL_JOIN_QUOTED_NAME}.#{model.quoted_primary_key}}
25
+ geom = {
26
+ class: model,
27
+ table_alias: SPATIAL_JOIN_NAME
28
+ }
29
+
30
+ if reflection.options[:geom].is_a?(Hash)
31
+ geom.merge!(reflection.options[:geom])
32
+ else
33
+ geom[:column] = reflection.options[:geom]
34
+ end
35
+
36
+ where_function = klass.send(
37
+ "st_#{reflection.options[:relationship]}",
38
+ geom,
39
+ (reflection.options[:scope_options] || {}).merge(
40
+ column: reflection.options[:foreign_geom]
41
+ )
42
+ )
43
+
44
+ scope.
45
+ select(%{#{klass.quoted_table_name}.*, array_to_string(array_agg(#{column}), ',') AS "#{SPATIAL_FIELD_ALIAS}"}).
46
+ joins(
47
+ "INNER JOIN #{join_name} AS #{SPATIAL_JOIN_QUOTED_NAME} ON (" <<
48
+ where_function.where_clause.send(:predicates).join(' AND ') <<
49
+ ')'
50
+ ).
51
+ where(model.arel_table.alias(SPATIAL_JOIN_NAME)[model.primary_key].in(ids)).
52
+ group(table[klass.primary_key])
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module ActiveRecord
3
+ module Reflection #:nodoc:
4
+ SPATIAL_REFLECTION_BASE_CLASS = HasManyReflection
5
+
6
+ module RuntimeReflectionWithSpatialReflection
7
+ delegate :geom, :relationship, :scope_options, to: :@reflection
8
+ end
9
+
10
+ RuntimeReflection.prepend RuntimeReflectionWithSpatialReflection
11
+
12
+ class SpatialReflection < SPATIAL_REFLECTION_BASE_CLASS #:nodoc:
13
+ attr_reader :geom, :foreign_geom, :relationship, :scope_options
14
+
15
+ def initialize(*args)
16
+ super(*args.from(1))
17
+
18
+ @geom = options[:geom]
19
+ @foreign_geom = options[:foreign_geom]
20
+ @relationship = options[:relationship].to_s
21
+ @scope_options = (options[:scope_options] || {}).merge(column: foreign_geom)
22
+ end
23
+
24
+ def association_class
25
+ Associations::SpatialAssociation
26
+ end
27
+ end
28
+
29
+ class << self
30
+ prepend(Module.new do
31
+ def create(macro, name, scope, options, ar)
32
+ if options[:relationship] && options[:geom] && options[:foreign_geom]
33
+ SpatialReflection.new(macro, name, scope, options, ar)
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end)
39
+ end
40
+ end
41
+ end
@@ -1,10 +1,32 @@
1
1
 
2
2
  require 'activerecord-spatial/associations/base'
3
+ require 'activerecord-spatial/associations/reflection/spatial_reflection'
4
+ require 'activerecord-spatial/associations/preloader/spatial_association'
5
+ require 'activerecord-spatial/associations/active_record'
3
6
 
4
- if ActiveRecord::VERSION::MAJOR <= 3
5
- require 'activerecord-spatial/associations/active_record_3'
6
- else
7
- require 'activerecord-spatial/associations/active_record'
7
+ module ActiveRecordSpatial::Associations
8
+ module ClassMethods #:nodoc:
9
+ def has_many_spatially(name, scope = nil, options = {}, &extension)
10
+ if scope.is_a?(Hash)
11
+ options = scope
12
+ scope = nil
13
+ end
14
+
15
+ options = build_options(options)
16
+
17
+ unless ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.include?(options[:relationship].to_s)
18
+ raise ArgumentError, %{Invalid spatial relationship "#{options[:relationship]}", expected one of #{ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.inspect}}
19
+ end
20
+
21
+ reflection = ActiveRecord::Associations::Builder::Spatial.build(self, name, scope, options, &extension)
22
+
23
+ if ActiveRecord::Reflection.respond_to?(:add_reflection)
24
+ ActiveRecord::Reflection.add_reflection(self, name, reflection)
25
+ end
26
+
27
+ reflection
28
+ end
29
+ end
8
30
  end
9
31
 
10
32
  module ActiveRecord
@@ -37,99 +37,97 @@ module ActiveRecordSpatial
37
37
  end
38
38
 
39
39
  module ClassMethods
40
- protected
41
- @geometry_columns = nil
42
- @geography_columns = nil
43
-
44
- public
45
- # Build call to ActiveRecordSpatial::SpatialFunction.build! that helps
46
- # you create spatial function calls.
47
- def spatial_function(*args)
48
- SpatialFunction.build!(self, *args)
49
- end
40
+ @geometry_columns = nil
41
+ @geography_columns = nil
50
42
 
51
- # Stubs for documentation purposes:
43
+ # Build call to ActiveRecordSpatial::SpatialFunction.build! that helps
44
+ # you create spatial function calls.
45
+ def spatial_function(*args)
46
+ SpatialFunction.build!(self, *args)
47
+ end
52
48
 
53
- # Returns an Array of available geometry columns in the
54
- # table. These are PostgreSQLColumns with values set for
55
- # the srid and coord_dimensions properties.
56
- def geometry_columns; end
49
+ # Stubs for documentation purposes:
57
50
 
58
- # Returns an Array of available geography columns in the
59
- # table. These are PostgreSQLColumns with values set for
60
- # the srid and coord_dimensions properties.
61
- def geography_columns; end
51
+ # Returns an Array of available geometry columns in the
52
+ # table. These are PostgreSQLColumns with values set for
53
+ # the srid and coord_dimensions properties.
54
+ def geometry_columns; end
62
55
 
63
- # Force a reload of available geometry columns.
64
- def geometry_columns!; end
56
+ # Returns an Array of available geography columns in the
57
+ # table. These are PostgreSQLColumns with values set for
58
+ # the srid and coord_dimensions properties.
59
+ def geography_columns; end
65
60
 
66
- # Force a reload of available geography columns.
67
- def geography_columns!; end
61
+ # Force a reload of available geometry columns.
62
+ def geometry_columns!; end
68
63
 
69
- # Grabs a geometry column based on name.
70
- def geometry_column_by_name(name); end
64
+ # Force a reload of available geography columns.
65
+ def geography_columns!; end
71
66
 
72
- # Grabs a geography column based on name.
73
- def geography_column_by_name(name); end
67
+ # Grabs a geometry column based on name.
68
+ def geometry_column_by_name(name); end
74
69
 
75
- # Returns both the geometry and geography columns for a table.
76
- def spatial_columns
77
- self.geometry_columns + self.geography_columns
78
- end
70
+ # Grabs a geography column based on name.
71
+ def geography_column_by_name(name); end
79
72
 
80
- # Reloads both the geometry and geography columns for a table.
81
- def spatial_columns!
82
- self.geometry_columns! + self.geography_columns!
83
- end
73
+ # Returns both the geometry and geography columns for a table.
74
+ def spatial_columns
75
+ geometry_columns + geography_columns
76
+ end
84
77
 
85
- # Grabs a spatial column based on name.
86
- def spatial_column_by_name(name)
87
- self.geometry_column_by_name(name) || self.geography_column_by_name(name)
88
- end
78
+ # Reloads both the geometry and geography columns for a table.
79
+ def spatial_columns!
80
+ geometry_columns! + geography_columns!
81
+ end
89
82
 
90
- %w{ geometry geography }.each do |m|
91
- src, line = <<-EOF, __LINE__ + 1
92
- undef :#{m}_columns
93
- def #{m}_columns
94
- if !defined?(@#{m}_columns) || @#{m}_columns.nil?
95
- @#{m}_columns = ActiveRecordSpatial::#{m.capitalize}Column.where(
96
- :f_table_name => self.table_name
97
- ).to_a
98
- @#{m}_columns.freeze
99
- end
100
- @#{m}_columns
101
- end
83
+ # Grabs a spatial column based on name.
84
+ def spatial_column_by_name(name)
85
+ geometry_column_by_name(name) || geography_column_by_name(name)
86
+ end
102
87
 
103
- undef :#{m}_columns!
104
- def #{m}_columns!
105
- @#{m}_columns = nil
106
- #{m}_columns
88
+ %w{ geometry geography }.each do |m|
89
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
90
+ undef :#{m}_columns
91
+ def #{m}_columns
92
+ if !defined?(@#{m}_columns) || @#{m}_columns.nil?
93
+ @#{m}_columns = ActiveRecordSpatial::#{m.capitalize}Column.where(
94
+ f_table_name: table_name
95
+ ).to_a
96
+ @#{m}_columns.freeze
107
97
  end
98
+ @#{m}_columns
99
+ end
108
100
 
109
- undef :#{m}_column_by_name
110
- def #{m}_column_by_name(name)
111
- @#{m}_column_by_name ||= self.#{m}_columns.inject(HashWithIndifferentAccess.new) do |memo, obj|
112
- memo[obj.spatial_column] = obj
113
- memo
114
- end
115
- @#{m}_column_by_name[name]
101
+ undef :#{m}_columns!
102
+ def #{m}_columns!
103
+ @#{m}_columns = nil
104
+ #{m}_columns
105
+ end
106
+
107
+ undef :#{m}_column_by_name
108
+ def #{m}_column_by_name(name)
109
+ @#{m}_column_by_name ||= #{m}_columns.inject(HashWithIndifferentAccess.new) do |memo, obj|
110
+ memo[obj.spatial_column] = obj
111
+ memo
116
112
  end
117
- EOF
118
- self.class_eval(src, __FILE__, line)
119
- end
113
+ @#{m}_column_by_name[name]
114
+ end
115
+ RUBY
116
+ end
120
117
 
121
- # Quickly grab the SRID for a geometry column.
122
- def srid_for(column_name)
123
- column = self.spatial_column_by_name(column_name)
124
- column.try(:srid) || ActiveRecordSpatial::UNKNOWN_SRID
125
- end
118
+ # Quickly grab the SRID for a geometry column.
119
+ def srid_for(column_name)
120
+ column = spatial_column_by_name(column_name)
121
+ column.try(:srid) || ActiveRecordSpatial::UNKNOWN_SRID
122
+ end
126
123
 
127
- # Quickly grab the number of dimensions for a geometry column.
128
- def coord_dimension_for(column_name)
129
- self.spatial_column_by_name(column_name).coord_dimension
130
- end
124
+ # Quickly grab the number of dimensions for a geometry column.
125
+ def coord_dimension_for(column_name)
126
+ spatial_column_by_name(column_name).coord_dimension
127
+ end
128
+
129
+ private
131
130
 
132
- protected
133
131
  # Sets up nifty setters and getters for spatial columns.
134
132
  # The methods created look like this:
135
133
  #
@@ -152,19 +150,14 @@ module ActiveRecordSpatial
152
150
  create_these = []
153
151
 
154
152
  if options.nil?
155
- create_these.concat(self.spatial_columns)
153
+ create_these.concat(spatial_columns)
156
154
  else
157
- if options[:geometry_columns]
158
- create_these.concat(self.geometry_columns)
159
- end
155
+ create_these.concat(geometry_columns) if options[:geometry_columns]
156
+ create_these.concat(geography_columns) if options[:geography_columns]
160
157
 
161
- if options[:geography_columns]
162
- create_these.concat(self.geography_columns)
163
- end
158
+ raise ArgumentError, "You can only specify either :except or :only (#{options.keys.inspect})" if options[:except] && options[:only]
164
159
 
165
- if options[:except] && options[:only]
166
- raise ArgumentError, "You can only specify either :except or :only (#{options.keys.inspect})"
167
- elsif options[:except]
160
+ if options[:except]
168
161
  except = Array.wrap(options[:except]).collect(&:to_s)
169
162
  create_these.reject! { |c| except.include?(c) }
170
163
  elsif options[:only]
@@ -174,7 +167,7 @@ module ActiveRecordSpatial
174
167
  end
175
168
 
176
169
  create_these.each do |k|
177
- src, line = <<-EOF, __LINE__ + 1
170
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
178
171
  def #{k.spatial_column}=(geom)
179
172
  if !geom
180
173
  self['#{k.spatial_column}'] = nil
@@ -236,16 +229,14 @@ module ActiveRecordSpatial
236
229
 
237
230
  self['#{k.spatial_column}']
238
231
  end
239
- EOF
240
- self.class_eval(src, __FILE__, line)
232
+ RUBY
241
233
 
242
234
  SPATIAL_COLUMN_OUTPUT_FORMATS.reject { |f| f == 'geos' }.each do |f|
243
- src, line = <<-EOF, __LINE__ + 1
235
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
244
236
  def #{k.spatial_column}_#{f}(*args)
245
237
  @#{k.spatial_column}_#{f} ||= self.#{k.spatial_column}_geos.to_#{f}(*args) rescue nil
246
238
  end
247
- EOF
248
- self.class_eval(src, __FILE__, line)
239
+ RUBY
249
240
  end
250
241
  end
251
242
  end
@@ -253,7 +244,7 @@ module ActiveRecordSpatial
253
244
  # Creates column accessors for geometry columns only.
254
245
  def create_geometry_column_accessors!(options = {})
255
246
  options = {
256
- :geometry_columns => true
247
+ geometry_columns: true
257
248
  }.merge(options)
258
249
 
259
250
  create_spatial_column_accessors!(options)
@@ -262,7 +253,7 @@ module ActiveRecordSpatial
262
253
  # Creates column accessors for geometry columns only.
263
254
  def create_geography_column_accessors!(options = {})
264
255
  options = {
265
- :geography_columns => true
256
+ geography_columns: true
266
257
  }.merge(options)
267
258
 
268
259
  create_spatial_column_accessors!(options)
@@ -323,7 +314,7 @@ module ActiveRecordSpatial
323
314
  # are equivalent:
324
315
  #
325
316
  # spatial_column_name(:wkt)
326
- # spatial_column_name(:format => :wkt)
317
+ # spatial_column_name(format: :wkt)
327
318
  # spatial_column_name_wkt
328
319
  def __spatial_column_name(options = {}); end
329
320