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.
- data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +24 -6
- data/lib/activerecord-spatial/associations.rb +5 -283
- data/lib/activerecord-spatial/associations/active_record.rb +146 -0
- data/lib/activerecord-spatial/associations/active_record_3.rb +123 -0
- data/lib/activerecord-spatial/associations/base.rb +182 -0
- data/lib/activerecord-spatial/spatial_columns.rb +1 -1
- data/lib/activerecord-spatial/spatial_function.rb +3 -2
- data/lib/activerecord-spatial/version.rb +1 -1
- data/test/associations_tests.rb +24 -4
- data/test/spatial_function_tests.rb +77 -0
- data/test/spatial_scopes_geographies_tests.rb +14 -14
- data/test/spatial_scopes_tests.rb +68 -68
- data/test/test_helper.rb +40 -31
- metadata +22 -11
- checksums.yaml +0 -15
@@ -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
|
+
|
@@ -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
|
-
|
20
|
-
|
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(
|
data/test/associations_tests.rb
CHANGED
@@ -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).
|
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').
|
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).
|
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
|
+
|