activerecord-spatial 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+