activerecord-spatial 0.0.1 → 0.1.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.
@@ -0,0 +1,123 @@
1
+
2
+ module ActiveRecord
3
+ module Associations
4
+ class Builder::Spatial < Builder::HasMany #:nodoc:
5
+ self.macro = SPATIAL_MACRO
6
+ self.valid_options += VALID_SPATIAL_OPTIONS
7
+ self.valid_options -= INVALID_SPATIAL_OPTIONS
8
+ end
9
+
10
+ class Preloader #:nodoc:
11
+ class SpatialAssociation < HasMany #:nodoc:
12
+ def records_for(ids)
13
+ table_name = reflection.quoted_table_name
14
+ join_name = model.quoted_table_name
15
+ column = %{#{SPATIAL_JOIN_QUOTED_NAME}.#{model.quoted_primary_key}}
16
+ geom = {
17
+ :class => model,
18
+ :table_alias => SPATIAL_JOIN_NAME
19
+ }
20
+
21
+ if reflection.options[:geom].is_a?(Hash)
22
+ geom.merge!(reflection.options[:geom])
23
+ else
24
+ geom[:column] = reflection.options[:geom]
25
+ end
26
+
27
+ scoped.
28
+ select(%{array_to_string(array_agg(#{column}), ',') AS "#{SPATIAL_FIELD_ALIAS}"}).
29
+ joins(
30
+ "INNER JOIN #{join_name} AS #{SPATIAL_JOIN_QUOTED_NAME} ON (" <<
31
+ klass.send("st_#{reflection.options[:relationship]}",
32
+ geom,
33
+ (reflection.options[:scope_options] || {}).merge(
34
+ :column => reflection.options[:foreign_geom]
35
+ )
36
+ ).where_values.join(' AND ') <<
37
+ ")"
38
+ ).
39
+ where(model.arel_table.alias(SPATIAL_JOIN_NAME)[model.primary_key].in(ids)).
40
+ group(table[klass.primary_key])
41
+ end
42
+ end
43
+ end
44
+
45
+ class AssociationScope #:nodoc:
46
+ def add_constraints_with_spatial(scope)
47
+ return add_constraints_without_spatial(scope) if !self.association.is_a?(SpatialAssociation)
48
+
49
+ tables = construct_tables
50
+
51
+ chain.each_with_index do |reflection, i|
52
+ table, foreign_table = tables.shift, tables.first
53
+
54
+ conditions = self.conditions[i]
55
+ geom_options = {
56
+ :class => self.association.klass
57
+ }
58
+
59
+ if self.association.geom.is_a?(Hash)
60
+ geom_options.merge!(
61
+ :value => owner[self.association.geom[:name]]
62
+ )
63
+ geom_options.merge!(self.association.geom)
64
+ else
65
+ geom_options.merge!(
66
+ :value => owner[self.association.geom],
67
+ :name => self.association.geom
68
+ )
69
+ end
70
+
71
+ if reflection == chain.last
72
+ scope = scope.send("st_#{self.association.relationship}", geom_options, self.association.scope_options)
73
+
74
+ if reflection.type
75
+ scope = scope.where(table[reflection.type].eq(owner.class.base_class.name))
76
+ end
77
+
78
+ conditions.each do |condition|
79
+ scope = scope.where(interpolate(condition))
80
+ end
81
+ else
82
+ constraint = scope.where(
83
+ scope.send(
84
+ "st_#{self.association.relationship}",
85
+ owner[self.association.foreign_geom],
86
+ self.association.scope_options
87
+ ).where_values
88
+ ).join(' AND ')
89
+
90
+ if reflection.type
91
+ type = chain[i + 1].klass.base_class.name
92
+ constraint = table[reflection.type].eq(type).and(constraint)
93
+ end
94
+
95
+ scope = scope.joins(join(foreign_table, constraint))
96
+
97
+ unless conditions.empty?
98
+ scope = scope.where(sanitize(conditions, table))
99
+ end
100
+ end
101
+ end
102
+
103
+ scope
104
+ end
105
+ alias_method_chain :add_constraints, :spatial
106
+ end
107
+ end
108
+ end
109
+
110
+ module ActiveRecordSpatial::Associations
111
+ module ClassMethods #:nodoc:
112
+ def has_many_spatially(name, options = {}, &extension)
113
+ options = build_options(options)
114
+
115
+ if !ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.include?(options[:relationship].to_s)
116
+ raise ArgumentError.new(%{Invalid spatial relationship "#{options[:relationship]}", expected one of #{ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.inspect}})
117
+ end
118
+
119
+ ActiveRecord::Associations::Builder::Spatial.build(self, name, options, &extension)
120
+ end
121
+ end
122
+ end
123
+
@@ -0,0 +1,182 @@
1
+
2
+ module ActiveRecord
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
+ class Builder::Spatial < Builder::HasMany #:nodoc:
20
+ SPATIAL_MACRO = :has_many
21
+
22
+ VALID_SPATIAL_OPTIONS = [
23
+ :geom, :foreign_geom, :relationship, :scope_options
24
+ ].freeze
25
+
26
+ INVALID_SPATIAL_OPTIONS = [
27
+ :through, :source, :source_type, :dependent, :finder_sql, :counter_sql,
28
+ :inverse_of
29
+ ].freeze
30
+
31
+ private
32
+ def dependency_method_name
33
+ "spatially_#{self.relationship}_dependent_for_#{name}"
34
+ end
35
+ end
36
+
37
+ class Preloader #:nodoc:
38
+ 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
68
+
69
+ def preloader_for_with_spatial(reflection)
70
+ if reflection.options[:relationship]
71
+ SpatialAssociation
72
+ else
73
+ preloader_for_without_spatial(reflection)
74
+ end
75
+ 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
90
+ end
91
+ end
92
+ end
93
+
94
+ # ActiveRecord Spatial associations allow for +has_many+-style associations
95
+ # using spatial relationships.
96
+ #
97
+ # == Example
98
+ #
99
+ # class Neighbourhood < ActiveRecord::Base
100
+ # has_many_spatially :cities,
101
+ # :relationship => :contains
102
+ # end
103
+ #
104
+ # class City < ActiveRecord::Base
105
+ # has_many_spatially :neighbourhoods,
106
+ # :relationship => :within
107
+ # end
108
+ #
109
+ # Neighbourhood.first.cities
110
+ # #=> All cities that the neighbourhood is within
111
+ #
112
+ # City.first.neighbourhoods
113
+ # #=> All neighbourhoods contained by the city
114
+ #
115
+ # City.includes(:neighbourhoods).first.neighbourhoods
116
+ # #=> Eager loading works too
117
+ #
118
+ # Spatial associations can be set up using any of the relationships found in
119
+ # ActiveRecordSpatial::SpatialScopes::RELATIONSHIPS.
120
+ #
121
+ # == Options
122
+ #
123
+ # Many of the options available with standard +has_many+ associations will work
124
+ # with the exceptions of +:through+, +:source+, +:source_type+, +:dependent+,
125
+ # +:finder_sql+, +:counter_sql+, and +:inverse_of+.
126
+ #
127
+ # Polymorphic relationships can be used via the +:as+ option as in standard
128
+ # +:has_many+ relationships. Note that the default field for the geometry
129
+ # in these cases is "#{association_name}_geom" and can be overridden using
130
+ # the +:foreign_geom+ option.
131
+ #
132
+ # * +:relationship+ - sets the spatial relationship for the association.
133
+ # Valid options can be found in ActiveRecordSpatial::SpatialScopes::RELATIONSHIPS.
134
+ # The default value is +:intersects+.
135
+ # * +:geom+ - sets the geometry field for the association in the calling model.
136
+ # The default value is +:the_geom+ as is often seen in PostGIS documentation.
137
+ # * +:foreign_geom+ - sets the geometry field for the association's foreign
138
+ # table. The default here is again +:the_geom+.
139
+ # * +:scope_options+ - these are options passed directly to the SpatialScopes
140
+ # 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
142
+ # spatial relationships to say "Foo spatially contains many Bars" and
143
+ # therefore the relationship in SQL becomes
144
+ # <tt>ST_contains("foos"."the_geom", "bars"."the_geom")</tt>.
145
+ #
146
+ # Note that you can modify the default geometry column name for all of
147
+ # ActiveRecordSpatial by setting it via ActiveRecordSpatia.default_column_name.
148
+ #
149
+ # == Caveats
150
+ #
151
+ # * You should consider spatial associations to be essentially readonly. Since
152
+ # we're not dealing with unique IDs here but rather 2D and 3D geometries,
153
+ # the relationships between rows don't really map well to the traditional
154
+ # foreign key-style ActiveRecord associations.
155
+ module ActiveRecordSpatial::Associations
156
+ extend ActiveSupport::Concern
157
+
158
+ DEFAULT_OPTIONS = {
159
+ :relationship => :intersects,
160
+ :geom => ActiveRecordSpatial.default_column_name,
161
+ :foreign_geom => ActiveRecordSpatial.default_column_name,
162
+ :scope_options => {
163
+ :invert => true
164
+ }
165
+ }.freeze
166
+
167
+ module ClassMethods #:nodoc:
168
+ def build_options(options)
169
+ if !options[:foreign_geom] && options[:as]
170
+ options[:foreign_geom] = "#{options[:as]}_geom"
171
+ end
172
+
173
+ if options[:geom].is_a?(Hash)
174
+ options[:geom][:name] ||= ActiveRecordSpatial.default_column_name
175
+ end
176
+
177
+ DEFAULT_OPTIONS.deep_merge(options)
178
+ end
179
+ private :build_options
180
+ end
181
+ end
182
+
@@ -94,7 +94,7 @@ module ActiveRecordSpatial
94
94
  if !defined?(@#{m}_columns) || @#{m}_columns.nil?
95
95
  @#{m}_columns = ActiveRecordSpatial::#{m.capitalize}Column.where(
96
96
  :f_table_name => self.table_name
97
- ).all
97
+ ).to_a
98
98
  @#{m}_columns.freeze
99
99
  end
100
100
  @#{m}_columns
@@ -16,8 +16,9 @@ module ActiveRecordSpatial
16
16
 
17
17
  def build_function_call(function, *args)
18
18
  options = default_options(args.extract_options!)
19
- geom = options[:geom_arg]
20
- args = Array.wrap(options[:args])
19
+
20
+ geom = options.fetch(:geom_arg, args.first)
21
+ args = Array.wrap(options.fetch(:args, args.from(1)))
21
22
 
22
23
  column_name = self.column_name(options[:column])
23
24
  first_geom_arg = self.wrap_column_or_geometry(
@@ -1,5 +1,5 @@
1
1
 
2
2
  module ActiveRecordSpatial
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
 
@@ -229,7 +229,7 @@ class PreloadTest < ActiveRecordSpatialTestCase
229
229
  values = nil
230
230
  assert_queries(4) do
231
231
  assert_sql(/ST_intersects\('#{REGEXP_WKB_HEX}'::geometry, "bars"\."the_geom"/) do
232
- values = Foo.all.collect do |foo|
232
+ values = Foo.all.to_a.collect do |foo|
233
233
  foo.bars.length
234
234
  end
235
235
  end
@@ -242,7 +242,7 @@ class PreloadTest < ActiveRecordSpatialTestCase
242
242
  values = nil
243
243
  assert_queries(2) do
244
244
  assert_sql(/SELECT "bars"\.\*, array_to_string\(array_agg\("__spatial_ids_join__"."id"\), ','\) AS "__spatial_ids__" FROM "bars" INNER JOIN "foos" AS "__spatial_ids_join__" ON \(ST_intersects\("__spatial_ids_join__"."the_geom", "bars"."the_geom"\)\) WHERE "__spatial_ids_join__"\."id" IN \(.+\) GROUP BY "bars"\."id"/) do
245
- values = Foo.includes(:bars).all.collect do |foo|
245
+ values = Foo.includes(:bars).to_a.collect do |foo|
246
246
  foo.bars.length
247
247
  end
248
248
  end
@@ -267,7 +267,7 @@ class PreloadWithOtherGeomTest < ActiveRecordSpatialTestCase
267
267
  values = nil
268
268
  assert_queries(4) do
269
269
  assert_sql(/ST_intersects\(ST_SetSRID\('#{REGEXP_WKB_HEX}'::geometry, #{ActiveRecordSpatial::UNKNOWN_SRID}\), "bars"\."the_geom"/) do
270
- values = Foo.order('id').all.collect do |foo|
270
+ values = Foo.order('id').to_a.collect do |foo|
271
271
  foo.bars.length
272
272
  end
273
273
  end
@@ -280,7 +280,7 @@ class PreloadWithOtherGeomTest < ActiveRecordSpatialTestCase
280
280
  values = nil
281
281
  assert_queries(2) do
282
282
  assert_sql(/SELECT "bars"\.\*, array_to_string\(array_agg\("__spatial_ids_join__"."id"\), ','\) AS "__spatial_ids__" FROM "bars" INNER JOIN "foos" AS "__spatial_ids_join__" ON \(ST_intersects\(ST_SetSRID\("__spatial_ids_join__"."the_other_geom", #{ActiveRecordSpatial::UNKNOWN_SRID}\), "bars"."the_geom"\)\) WHERE "__spatial_ids_join__"\."id" IN \(.+\) GROUP BY "bars"\."id"/) do
283
- values = Foo.order('id').includes(:bars).all.collect do |foo|
283
+ values = Foo.order('id').includes(:bars).to_a.collect do |foo|
284
284
  foo.bars.length
285
285
  end
286
286
  end
@@ -460,6 +460,8 @@ class IncludeOptionTest < ActiveRecordSpatialTestCase
460
460
  end
461
461
 
462
462
  def test_includes
463
+ skip("Removed from AR 4") if ActiveRecord::VERSION::MAJOR >= 4
464
+
463
465
  values = nil
464
466
  assert_queries(3) do
465
467
  assert_sql(/SELECT\s+"blorts"\.\*\s+FROM\s+"blorts"\s+WHERE\s+"blorts"\."foo_id"\s+IN\s+\(.+\)/) do
@@ -652,5 +654,23 @@ class BothGeomWrapperAndOptionsWithMixedSRIDsTest < ActiveRecordSpatialTestCase
652
654
 
653
655
  assert_equal([ 1, 2, 3 ], values)
654
656
  end
657
+
658
+ class ScopeArgumentTest < ActiveRecordSpatialTestCase
659
+ def setup
660
+ self.class.load_models(:foo, :bar, :blort)
661
+ end
662
+
663
+ def test_foo
664
+ Foo.class_eval do
665
+ has_many_spatially :bars, proc {
666
+ self.order(:id)
667
+ }
668
+ end
669
+
670
+ assert_sql(/ORDER BY "bars".id/) do
671
+ Foo.first.bars.to_a
672
+ end
673
+ end
674
+ end if ActiveRecord::VERSION::MAJOR >= 4
655
675
  end
656
676
 
@@ -0,0 +1,77 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class SpatialFunctionTests < ActiveRecordSpatialTestCase
6
+ def self.before_suite
7
+ load_models(:foo)
8
+ load_models(:blort)
9
+ end
10
+
11
+ def test_geom_arg_option
12
+ assert_equal(
13
+ %{ST_distance("foos"."the_geom", '010100000000000000000000000000000000000000'::geometry)},
14
+ Foo.spatial_function(:distance, :geom_arg => 'POINT(0 0)').to_sql
15
+ )
16
+ end
17
+
18
+ def test_geom_as_argument
19
+ assert_equal(
20
+ %{ST_distance("foos"."the_geom", '010100000000000000000000000000000000000000'::geometry)},
21
+ Foo.spatial_function(:distance, 'POINT(0 0)').to_sql
22
+ )
23
+ end
24
+
25
+ def test_column_option
26
+ assert_equal(
27
+ %{ST_distance("foos"."the_other_geom", ST_SetSRID('010100000000000000000000000000000000000000'::geometry, 4326))},
28
+ Foo.spatial_function(:distance, 'POINT(0 0)', :column => 'the_other_geom').to_sql
29
+ )
30
+ end
31
+
32
+ def test_class_option
33
+ assert_equal(
34
+ %{ST_distance("foos"."the_other_geom", ST_SetSRID('010100000000000000000000000000000000000000'::geometry, 4326))},
35
+ Foo.spatial_function(:distance, {
36
+ :class => Blort,
37
+ :value => 'POINT(0 0)'
38
+ }, {
39
+ :column => 'the_other_geom'
40
+ }).to_sql
41
+ )
42
+ end
43
+
44
+ def test_class_name_option
45
+ assert_equal(
46
+ %{ST_distance("foos"."the_other_geom", ST_SetSRID('010100000000000000000000000000000000000000'::geometry, 4326))},
47
+ Foo.spatial_function(:distance, {
48
+ :class => 'Blort',
49
+ :value => 'POINT(0 0)'
50
+ }, {
51
+ :column => 'the_other_geom'
52
+ }).to_sql
53
+ )
54
+ end
55
+
56
+ def test_invert_option
57
+ assert_equal(
58
+ %{ST_distance('010100000000000000000000000000000000000000'::geometry, "foos"."the_geom")},
59
+ Foo.spatial_function(:distance, 'POINT(0 0)', :invert => true).to_sql
60
+ )
61
+ end
62
+
63
+ def test_use_index_option
64
+ assert_equal(
65
+ %{_ST_distance("foos"."the_geom", '010100000000000000000000000000000000000000'::geometry)},
66
+ Foo.spatial_function(:distance, 'POINT(0 0)', :use_index => false).to_sql
67
+ )
68
+ end
69
+
70
+ def test_allow_null_option
71
+ assert_equal(
72
+ %{(ST_distance("foos"."the_geom", '010100000000000000000000000000000000000000'::geometry) OR "foos"."the_geom" IS NULL)},
73
+ Foo.spatial_function(:distance, 'POINT(0 0)', :allow_null => true).to_sql
74
+ )
75
+ end
76
+ end
77
+