activerecord-spatial 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +3314 -1052
  3. data/.travis.yml +17 -0
  4. data/Gemfile +1 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.rdoc +6 -12
  7. data/activerecord-spatial.gemspec +1 -1
  8. data/lib/activerecord-spatial.rb +1 -0
  9. data/lib/activerecord-spatial/active_record.rb +1 -0
  10. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions.rb +1 -0
  11. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions/active_record.rb +4 -3
  12. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/postgis.rb +1 -0
  13. data/lib/activerecord-spatial/active_record/connection_adapters/postgresql/unknown_srid.rb +2 -1
  14. data/lib/activerecord-spatial/active_record/models/geography_column.rb +1 -0
  15. data/lib/activerecord-spatial/active_record/models/geometry_column.rb +1 -0
  16. data/lib/activerecord-spatial/active_record/models/spatial_column.rb +1 -0
  17. data/lib/activerecord-spatial/active_record/models/spatial_ref_sys.rb +1 -0
  18. data/lib/activerecord-spatial/associations.rb +4 -12
  19. data/lib/activerecord-spatial/associations/active_record.rb +96 -54
  20. data/lib/activerecord-spatial/associations/base.rb +2 -19
  21. data/lib/activerecord-spatial/associations/reflection/spatial_reflection.rb +1 -0
  22. data/lib/activerecord-spatial/associations/spatial_association.rb +41 -0
  23. data/lib/activerecord-spatial/spatial_columns.rb +1 -0
  24. data/lib/activerecord-spatial/spatial_function.rb +2 -1
  25. data/lib/activerecord-spatial/spatial_scope_constants.rb +1 -0
  26. data/lib/activerecord-spatial/spatial_scope_constants/postgis_2_0.rb +1 -0
  27. data/lib/activerecord-spatial/spatial_scope_constants/postgis_2_2.rb +1 -0
  28. data/lib/activerecord-spatial/spatial_scope_constants/postgis_legacy.rb +1 -0
  29. data/lib/activerecord-spatial/spatial_scopes.rb +14 -13
  30. data/lib/activerecord-spatial/version.rb +2 -1
  31. data/test/accessors_geographies_tests.rb +2 -1
  32. data/test/accessors_geometries_tests.rb +2 -1
  33. data/test/adapter_tests.rb +5 -4
  34. data/test/associations_tests.rb +40 -25
  35. data/test/geography_column_tests.rb +2 -1
  36. data/test/geometry_column_tests.rb +2 -1
  37. data/test/models/bar.rb +1 -9
  38. data/test/models/blort.rb +1 -7
  39. data/test/models/foo.rb +3 -9
  40. data/test/models/foo3d.rb +1 -9
  41. data/test/models/foo_geography.rb +1 -8
  42. data/test/models/zortable.rb +1 -9
  43. data/test/schema.rb +54 -0
  44. data/test/spatial_function_tests.rb +2 -1
  45. data/test/spatial_scopes_geographies_tests.rb +2 -1
  46. data/test/spatial_scopes_tests.rb +2 -1
  47. data/test/test_helper.rb +57 -91
  48. metadata +9 -7
  49. data/lib/activerecord-spatial/associations/preloader/spatial_association.rb +0 -57
@@ -0,0 +1,17 @@
1
+ cache: bundler
2
+ sudo: false
3
+ language: ruby
4
+ rvm:
5
+ - 2.4
6
+ - 2.5
7
+ - 2.6
8
+ - rbx-3
9
+ - jruby
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: rbx-3
13
+ - rvm: jruby
14
+ addons:
15
+ apt:
16
+ packages:
17
+ - libgeos-c1
data/Gemfile CHANGED
@@ -16,5 +16,6 @@ gem 'minitest-reporters'
16
16
  gem 'rake'
17
17
  gem 'rdoc'
18
18
  gem 'simplecov'
19
+ gem 'terminal-notifier'
19
20
 
20
21
  instance_eval File.read('Gemfile.local') if File.exist?('Gemfile.local')
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2017 J Smith <dark.panda@gmail.com>
1
+ Copyright (c) 2010-2019 J Smith <dark.panda@gmail.com>
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
@@ -107,21 +107,15 @@ See the test/database.yml file for example settings.
107
107
 
108
108
  == ActiveRecord Versions Supported
109
109
 
110
- We're starting to standardize on Rails 5 for our purposes, so future versions
111
- this gem function on versions of Rails 5 and above. To use spatial scopes on
112
- previous versions of ActiveRecord, see our geos-extensions gem available at
113
- https://github.com/dark-panda/geos-extensions . We have pulled the ActiveRecord
114
- extensions out of that gem and are instead packaging them here, thus allowing
115
- you to use the actual Geos extensions in an unfettered way. If you wish to
116
- use the spatial extensions with versions of Rails prior to 5, see versions of
117
- the ActiveRecordSpatial gem prior to version 1.0.0.
110
+ Major version updates now coincide with Rails version updates. This particular
111
+ version works with Rails 6.0. For older versions of Rails, see older versions of
112
+ the gem.
118
113
 
119
114
  == PostGIS and PostgreSQL Versions Supported
120
115
 
121
- As of this writing, things we test on PostgreSQL 9.4 and PostGIS 2.1+.
122
- Some features are only available in PostGIS 2.0, such as the +st_3dintersects+
123
- scope and the like, as that spatial relationship function was added in PostGIS
124
- 2.0.
116
+ As of this writing, things we test on PostgreSQL 11 and PostGIS 2.5+.
117
+ Some features are only available on newer versions of PostGIS such as certain
118
+ scopes and the like.
125
119
 
126
120
  PostgreSQL 9.0 and below are currently not supported as some of the SQL we
127
121
  produce to create the spatial queries is not supported in older PostgreSQL
@@ -21,6 +21,6 @@ Gem::Specification.new do |s|
21
21
  s.homepage = 'https://github.com/dark-panda/activerecord-spatial'
22
22
  s.require_paths = ['lib']
23
23
 
24
- s.add_dependency('rails', ['>= 5.0'])
24
+ s.add_dependency('rails', ['>= 6.0'])
25
25
  s.add_dependency('geos-extensions', ['>= 0.5'])
26
26
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'geos-extensions'
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  autoload :SpatialFunction, 'activerecord-spatial/spatial_function'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'active_record/connection_adapters/postgresql_adapter'
3
4
  require 'activerecord-spatial/active_record/connection_adapters/postgresql/adapter_extensions/active_record'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module Type
@@ -37,10 +38,10 @@ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:geom
37
38
 
38
39
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
39
40
  prepend(Module.new do
40
- def initialize_type_map(type_map)
41
+ def initialize_type_map(m = type_map)
41
42
  super
42
- type_map.register_type 'geometry', ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Geometry.new
43
- type_map.register_type 'geography', ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Geography.new
43
+ m.register_type 'geometry', ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Geometry.new
44
+ m.register_type 'geography', ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Geography.new
44
45
  end
45
46
  end)
46
47
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  POSTGIS = begin
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  UNKNOWN_SRIDS = begin
@@ -8,7 +9,7 @@ module ActiveRecordSpatial
8
9
  }.freeze
9
10
  else
10
11
  {
11
- geography: 0,
12
+ geography: 0,
12
13
  geometry: -1
13
14
  }.freeze
14
15
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  class GeographyColumn < ActiveRecord::Base
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  class GeometryColumn < ActiveRecord::Base
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  module SpatialColumn #:nodoc:
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecordSpatial
3
4
  class SpatialRefSys < ::ActiveRecord::Base
@@ -1,17 +1,13 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  require 'activerecord-spatial/associations/base'
3
4
  require 'activerecord-spatial/associations/reflection/spatial_reflection'
4
- require 'activerecord-spatial/associations/preloader/spatial_association'
5
+ require 'activerecord-spatial/associations/spatial_association'
5
6
  require 'activerecord-spatial/associations/active_record'
6
7
 
7
8
  module ActiveRecordSpatial::Associations
8
9
  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
-
10
+ def has_many_spatially(name, scope = nil, **options, &extension)
15
11
  options = build_options(options)
16
12
 
17
13
  unless ActiveRecordSpatial::SpatialScopeConstants::RELATIONSHIPS.include?(options[:relationship].to_s)
@@ -20,11 +16,7 @@ module ActiveRecordSpatial::Associations
20
16
 
21
17
  reflection = ActiveRecord::Associations::Builder::Spatial.build(self, name, scope, options, &extension)
22
18
 
23
- if ActiveRecord::Reflection.respond_to?(:add_reflection)
24
- ActiveRecord::Reflection.add_reflection(self, name, reflection)
25
- end
26
-
27
- reflection
19
+ ActiveRecord::Reflection.add_reflection(self, name, reflection)
28
20
  end
29
21
  end
30
22
  end
@@ -1,69 +1,35 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module Associations #:nodoc:
4
- class Preloader #:nodoc:
5
- class SpatialAssociation < HasMany #:nodoc:
6
- private
5
+ class SpatialAssociationScope < AssociationScope #:nodoc:
6
+ INSTANCE = create
7
7
 
8
- def load_records(&block)
9
- return {} if owner_keys.empty?
8
+ class << self
9
+ def scope(association)
10
+ ActiveRecord::Associations::SpatialAssociationScope::INSTANCE.scope(association)
11
+ end
10
12
 
11
- slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
12
- @preloaded_records = slices.flat_map do |slice|
13
- records_for(slice).load(&block)
14
- end
13
+ def get_bind_values(owner, chain)
14
+ binds = []
15
+ last_reflection = chain.last
15
16
 
16
- @preloaded_records.each_with_object({}) do |record, memo|
17
- keys = record[association_key_name].split(',')
18
- keys.each do |key|
19
- memo[key] ||= []
20
- memo[key] << record
21
- end
22
- end
17
+ if last_reflection.type
18
+ binds << owner.class.polymorphic_name
23
19
  end
24
20
 
25
- def associated_records_by_owner(_preloader)
26
- records = load_records do |record|
27
- record[association_key_name].split(',').each do |key|
28
- owner = owners_by_key[convert_key(key)]
29
- association = owner.association(reflection.name)
30
- association.set_inverse_instance(record)
31
- end
32
- end
33
-
34
- owners.each_with_object({}) do |owner, result|
35
- result[owner] = records[convert_key(owner[owner_key_name])] || []
21
+ chain.each_cons(2).each do |reflection, next_reflection|
22
+ if reflection.type
23
+ binds << next_reflection.klass.polymorphic_name
36
24
  end
37
25
  end
38
- end
39
- end
40
-
41
- class SpatialAssociation < HasManyAssociation #:nodoc:
42
- def association_scope
43
- return unless klass
44
-
45
- @association_scope ||= SpatialAssociationScope.scope(self, klass.connection)
46
- end
47
-
48
- private
49
-
50
- def get_records
51
- scope.to_a
52
- end
53
- end
54
-
55
- class SpatialAssociationScope < AssociationScope #:nodoc:
56
- INSTANCE = create
57
-
58
- class << self
59
- def scope(association, connection)
60
- INSTANCE.scope(association, connection)
26
+ binds
61
27
  end
62
28
  end
63
29
 
64
- def last_chain_scope(scope, table, reflection, owner, assoc_klass)
30
+ def last_chain_scope(scope, reflection, owner)
65
31
  geom_options = {
66
- class: assoc_klass
32
+ class: reflection.klass
67
33
  }
68
34
 
69
35
  if reflection.geom.is_a?(Hash)
@@ -74,15 +40,91 @@ module ActiveRecord
74
40
  geom_options[:name] = reflection.geom
75
41
  end
76
42
 
43
+ table = reflection.aliased_table
77
44
  scope = scope.send("st_#{reflection.relationship}", geom_options, reflection.scope_options)
78
45
 
79
46
  if reflection.type
80
- polymorphic_type = transform_value(owner.class.base_class.name)
81
- scope = scope.where(table.name => { reflection.type => polymorphic_type })
47
+ polymorphic_type = transform_value(owner.class.polymorphic_name)
48
+ scope = apply_scope(scope, table, reflection.type, polymorphic_type)
82
49
  end
83
50
 
84
51
  scope
85
52
  end
86
53
  end
54
+
55
+ class Preloader #:nodoc:
56
+ class SpatialAssociation < Association #:nodoc:
57
+ SPATIAL_FIELD_ALIAS = '__spatial_ids__'
58
+ SPATIAL_JOIN_NAME = '__spatial_ids_join__'
59
+ SPATIAL_JOIN_QUOTED_NAME = %{"#{SPATIAL_JOIN_NAME}"}
60
+
61
+ private
62
+
63
+ def records_for(ids)
64
+ join_name = reflection.active_record.quoted_table_name
65
+ column = %{#{SPATIAL_JOIN_QUOTED_NAME}.#{klass.quoted_primary_key}}
66
+ geom = {
67
+ class: reflection.active_record,
68
+ table_alias: SPATIAL_JOIN_NAME
69
+ }
70
+
71
+ if reflection.options[:geom].is_a?(Hash)
72
+ geom.merge!(reflection.options[:geom])
73
+ else
74
+ geom[:column] = reflection.options[:geom]
75
+ end
76
+
77
+ where_function = klass.send(
78
+ "st_#{reflection.options[:relationship]}",
79
+ geom,
80
+ (reflection.options[:scope_options] || {}).merge(
81
+ column: reflection.options[:foreign_geom]
82
+ )
83
+ )
84
+
85
+ spatial_scope = scope
86
+ .select(%{#{klass.quoted_table_name}.*, array_to_string(array_agg(#{column}), ',') AS "#{SPATIAL_FIELD_ALIAS}"})
87
+ .joins(
88
+ "INNER JOIN #{join_name} AS #{SPATIAL_JOIN_QUOTED_NAME} ON (" +
89
+ where_function.where_clause.send(:predicates).join(' AND ') +
90
+ ')'
91
+ )
92
+ .where(klass.arel_table.alias(SPATIAL_JOIN_NAME)[klass.primary_key].in(ids))
93
+ .group(klass.arel_table[klass.primary_key])
94
+
95
+ spatial_scope.load do |record|
96
+ record[SPATIAL_FIELD_ALIAS].split(',').each do |spatial_id|
97
+ owner = owners_by_key[convert_key(spatial_id)].first
98
+
99
+ association = owner.association(reflection.name)
100
+ association.set_inverse_instance(record)
101
+ end
102
+ end
103
+ end
104
+
105
+ def records_by_owner
106
+ @records_by_owner ||= preloaded_records.each_with_object({}.compare_by_identity) do |record, result|
107
+ record[SPATIAL_FIELD_ALIAS].split(',').each do |spatial_id|
108
+ owners_by_key[convert_key(spatial_id)].each do |owner|
109
+ (result[owner] ||= []) << record
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ prepend(Module.new do
119
+ def preloader_for(reflection, owners)
120
+ return super unless reflection.is_a?(ActiveRecord::Reflection::SpatialReflection)
121
+ return AlreadyLoaded if owners.first.association(reflection.name).loaded?
122
+
123
+ reflection.check_preloadable!
124
+
125
+ SpatialAssociation
126
+ end
127
+ end)
128
+ end
87
129
  end
88
130
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module Associations #:nodoc:
@@ -13,7 +14,7 @@ module ActiveRecord
13
14
  :inverse_of
14
15
  ].freeze
15
16
 
16
- def macro
17
+ def self.macro
17
18
  SPATIAL_MACRO
18
19
  end
19
20
 
@@ -21,24 +22,6 @@ module ActiveRecord
21
22
  super + VALID_SPATIAL_OPTIONS - INVALID_SPATIAL_OPTIONS
22
23
  end
23
24
  end
24
-
25
- class Preloader #:nodoc:
26
- class SpatialAssociation < HasMany #:nodoc:
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
31
-
32
- prepend(Module.new do
33
- def preloader_for(reflection, *args)
34
- if reflection.is_a?(ActiveRecord::Reflection::SpatialReflection)
35
- SpatialAssociation
36
- else
37
- super
38
- end
39
- end
40
- end)
41
- end
42
25
  end
43
26
  end
44
27
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module Reflection #:nodoc:
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class SpatialAssociation < HasManyAssociation #:nodoc:
6
+ def association_scope
7
+ return unless klass
8
+
9
+ @association_scope ||= SpatialAssociationScope.scope(self)
10
+ end
11
+
12
+ private
13
+
14
+ def find_target
15
+ scope = self.scope
16
+ return scope.to_a if skip_statement_cache?(scope)
17
+
18
+ conn = klass.connection
19
+
20
+ # Since we're not using binds, we need to disable the scope cache,
21
+ # basically, as otherwise the non-bound parameters we use will cause
22
+ # cache misses that basically ignore subsequent scopes. This would
23
+ # be much better to remove completely, but this will do for now
24
+ # until we can find a better solution.
25
+ reflection.clear_association_scope_cache
26
+
27
+ sc = reflection.association_scope_cache(conn, owner) do |params|
28
+ as = SpatialAssociationScope.create { params.bind }
29
+ target_scope.merge!(as.scope(self))
30
+ end
31
+
32
+ binds = SpatialAssociationScope.get_bind_values(owner, reflection.chain)
33
+ sc.execute(binds, conn) { |record| set_inverse_instance(record) } || []
34
+ end
35
+
36
+ def association_key_name
37
+ SPATIAL_FIELD_ALIAS
38
+ end
39
+ end
40
+ end
41
+ end