activerecord-spatial 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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