activerecord-postgis-array 0.3.4

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 (97) hide show
  1. data/.gitignore +21 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +18 -0
  4. data/CHANGELOG.md +99 -0
  5. data/CONTRIBUTING.md +35 -0
  6. data/Gemfile +12 -0
  7. data/LICENSE +22 -0
  8. data/README.md +87 -0
  9. data/Rakefile +33 -0
  10. data/activerecord-postgis-array.gemspec +30 -0
  11. data/docs/indexes.md +28 -0
  12. data/docs/migrations.md +92 -0
  13. data/docs/querying.md +170 -0
  14. data/docs/type_casting.md +51 -0
  15. data/lib/activerecord-postgis-array.rb +3 -0
  16. data/lib/activerecord-postgis-array/active_record.rb +4 -0
  17. data/lib/activerecord-postgis-array/active_record/connection_adapters.rb +1 -0
  18. data/lib/activerecord-postgis-array/active_record/connection_adapters/postgres_adapter.rb +346 -0
  19. data/lib/activerecord-postgis-array/active_record/relation.rb +2 -0
  20. data/lib/activerecord-postgis-array/active_record/relation/predicate_builder.rb +71 -0
  21. data/lib/activerecord-postgis-array/active_record/relation/query_methods.rb +84 -0
  22. data/lib/activerecord-postgis-array/active_record/sanitization.rb +30 -0
  23. data/lib/activerecord-postgis-array/active_record/schema_dumper.rb +157 -0
  24. data/lib/activerecord-postgis-array/arel.rb +3 -0
  25. data/lib/activerecord-postgis-array/arel/nodes.rb +2 -0
  26. data/lib/activerecord-postgis-array/arel/nodes/array_nodes.rb +9 -0
  27. data/lib/activerecord-postgis-array/arel/nodes/contained_within.rb +20 -0
  28. data/lib/activerecord-postgis-array/arel/predications.rb +25 -0
  29. data/lib/activerecord-postgis-array/arel/visitors.rb +2 -0
  30. data/lib/activerecord-postgis-array/arel/visitors/to_sql.rb +15 -0
  31. data/lib/activerecord-postgis-array/arel/visitors/visitor.rb +38 -0
  32. data/lib/activerecord-postgis-array/version.rb +3 -0
  33. data/spec/arel/arel_spec.rb +30 -0
  34. data/spec/arel/array_spec.rb +77 -0
  35. data/spec/columns/array_spec.rb +120 -0
  36. data/spec/dummy/.gitignore +15 -0
  37. data/spec/dummy/README.rdoc +261 -0
  38. data/spec/dummy/Rakefile +7 -0
  39. data/spec/dummy/app/assets/images/rails.png +0 -0
  40. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  41. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  42. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  43. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  44. data/spec/dummy/app/mailers/.gitkeep +0 -0
  45. data/spec/dummy/app/models/.gitkeep +0 -0
  46. data/spec/dummy/app/models/person.rb +3 -0
  47. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/spec/dummy/config.ru +4 -0
  49. data/spec/dummy/config/application.rb +59 -0
  50. data/spec/dummy/config/boot.rb +6 -0
  51. data/spec/dummy/config/database.yml.example +14 -0
  52. data/spec/dummy/config/environment.rb +5 -0
  53. data/spec/dummy/config/environments/development.rb +38 -0
  54. data/spec/dummy/config/environments/production.rb +67 -0
  55. data/spec/dummy/config/environments/test.rb +37 -0
  56. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  57. data/spec/dummy/config/initializers/inflections.rb +15 -0
  58. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  59. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  60. data/spec/dummy/config/initializers/session_store.rb +8 -0
  61. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  62. data/spec/dummy/config/locales/en.yml +5 -0
  63. data/spec/dummy/config/routes.rb +58 -0
  64. data/spec/dummy/db/migrate/20120501163758_create_people.rb +12 -0
  65. data/spec/dummy/db/schema.rb +25 -0
  66. data/spec/dummy/db/seeds.rb +7 -0
  67. data/spec/dummy/lib/assets/.gitkeep +0 -0
  68. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  69. data/spec/dummy/log/.gitkeep +0 -0
  70. data/spec/dummy/public/404.html +26 -0
  71. data/spec/dummy/public/422.html +26 -0
  72. data/spec/dummy/public/500.html +25 -0
  73. data/spec/dummy/public/favicon.ico +0 -0
  74. data/spec/dummy/public/index.html +241 -0
  75. data/spec/dummy/public/robots.txt +5 -0
  76. data/spec/dummy/script/rails +6 -0
  77. data/spec/dummy/spec/factories/people.rb +7 -0
  78. data/spec/dummy/test/fixtures/.gitkeep +0 -0
  79. data/spec/dummy/test/functional/.gitkeep +0 -0
  80. data/spec/dummy/test/integration/.gitkeep +0 -0
  81. data/spec/dummy/test/performance/browsing_test.rb +12 -0
  82. data/spec/dummy/test/test_helper.rb +13 -0
  83. data/spec/dummy/test/unit/.gitkeep +0 -0
  84. data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
  85. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  86. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  87. data/spec/migrations/active_record_migration_spec.rb +29 -0
  88. data/spec/migrations/array_spec.rb +136 -0
  89. data/spec/migrations/index_spec.rb +67 -0
  90. data/spec/models/array_spec.rb +285 -0
  91. data/spec/queries/array_queries_spec.rb +72 -0
  92. data/spec/queries/sanity_spec.rb +16 -0
  93. data/spec/schema_dumper/array_spec.rb +17 -0
  94. data/spec/schema_dumper/extension_spec.rb +14 -0
  95. data/spec/schema_dumper/index_spec.rb +46 -0
  96. data/spec/spec_helper.rb +29 -0
  97. metadata +318 -0
@@ -0,0 +1,2 @@
1
+ require 'activerecord-postgis-array/active_record/relation/query_methods'
2
+ require 'activerecord-postgis-array/active_record/relation/predicate_builder'
@@ -0,0 +1,71 @@
1
+ require 'active_record/relation/predicate_builder'
2
+
3
+ module ActiveRecord
4
+ class PredicateBuilder # :nodoc:
5
+ def self.build_from_hash(engine, attributes, default_table, allow_table_name = true)
6
+ predicates = attributes.map do |column, value|
7
+ table = default_table
8
+
9
+ if allow_table_name && value.is_a?(Hash)
10
+ table = Arel::Table.new(column, engine)
11
+
12
+ if value.empty?
13
+ '1 = 2'
14
+ else
15
+ build_from_hash(engine, value, table, false)
16
+ end
17
+ else
18
+ column = column.to_s
19
+
20
+ if allow_table_name && column.include?('.')
21
+ table_name, column = column.split('.', 2)
22
+ table = Arel::Table.new(table_name, engine)
23
+ end
24
+
25
+ attribute = table[column.to_sym]
26
+
27
+ case value
28
+ when ActiveRecord::Relation
29
+ value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
30
+ attribute.in(value.arel.ast)
31
+ when Array, ActiveRecord::Associations::CollectionProxy
32
+ column_definition = engine.columns.find { |col| col.name == column }
33
+
34
+ if column_definition.respond_to?(:array) && column_definition.array
35
+ attribute.eq(value)
36
+ else
37
+ values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
38
+ ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
39
+
40
+ array_predicates = ranges.map {|range| attribute.in(range)}
41
+
42
+ if values.include?(nil)
43
+ values = values.compact
44
+ if values.empty?
45
+ array_predicates << attribute.eq(nil)
46
+ else
47
+ array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
48
+ end
49
+ else
50
+ array_predicates << attribute.in(values)
51
+ end
52
+
53
+ array_predicates.inject {|composite, predicate| composite.or(predicate)}
54
+ end
55
+ when Range, Arel::Relation
56
+ attribute.in(value)
57
+ when ActiveRecord::Base
58
+ attribute.eq(value.id)
59
+ when Class
60
+ # FIXME: I think we need to deprecate this behavior
61
+ attribute.eq(value.name)
62
+ else
63
+ attribute.eq(value)
64
+ end
65
+ end
66
+ end
67
+
68
+ predicates.flatten
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,84 @@
1
+ require 'active_record/relation/query_methods'
2
+
3
+ module ActiveRecord
4
+ module QueryMethods
5
+ class WhereChain
6
+ def initialize(scope)
7
+ @scope = scope
8
+ end
9
+
10
+ def overlap(opts)
11
+ opts.each do |key, value|
12
+ @scope = @scope.where(arel_table[key].overlap(value))
13
+ end
14
+ @scope
15
+ end
16
+
17
+ def contained_within(opts)
18
+ opts.each do |key, value|
19
+ @scope = @scope.where(arel_table[key].contained_within(value))
20
+ end
21
+
22
+ @scope
23
+ end
24
+
25
+ def contained_within_or_equals(opts)
26
+ opts.each do |key, value|
27
+ @scope = @scope.where(arel_table[key].contained_within_or_equals(value))
28
+ end
29
+
30
+ @scope
31
+ end
32
+
33
+ def contains(opts)
34
+ opts.each do |key, value|
35
+ @scope = @scope.where(arel_table[key].contains(value))
36
+ end
37
+
38
+ @scope
39
+ end
40
+
41
+ def contains_or_equals(opts)
42
+ opts.each do |key, value|
43
+ @scope = @scope.where(arel_table[key].contains_or_equals(value))
44
+ end
45
+
46
+ @scope
47
+ end
48
+
49
+ def any(opts)
50
+ equality_to_function('ANY', opts)
51
+ end
52
+
53
+ def all(opts)
54
+ equality_to_function('ALL', opts)
55
+ end
56
+
57
+ private
58
+
59
+ def arel_table
60
+ @arel_table ||= @scope.engine.arel_table
61
+ end
62
+
63
+ def equality_to_function(function_name, opts)
64
+ opts.each do |key, value|
65
+ any_function = Arel::Nodes::NamedFunction.new(function_name, [arel_table[key]])
66
+ predicate = Arel::Nodes::Equality.new(value, any_function)
67
+ @scope = @scope.where(predicate)
68
+ end
69
+
70
+ @scope
71
+ end
72
+ end
73
+
74
+ def where_with_chaining(*opts, &block)
75
+ if opts.empty? && !block_given?
76
+ WhereChain.new(self)
77
+ else
78
+ where_without_chaining(*opts, &block)
79
+ end
80
+ end
81
+
82
+ alias_method_chain :where, :chaining
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_record/sanitization'
2
+
3
+ module ActiveRecord
4
+ module Sanitization
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def sanitize_sql_hash_for_assignment(attrs)
9
+ attrs.map do |attr, value|
10
+ "#{connection.quote_column_name(attr)} = #{quote_bound_value(value, attr)}"
11
+ end.join(', ')
12
+ end
13
+
14
+ def quote_bound_value(value, column = nil, c = connection)
15
+ if column.present? && column != c
16
+ record_column = self.columns.select {|col| col.name == column}.first
17
+ c.quote(value, record_column)
18
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
19
+ if value.respond_to?(:empty?) && value.empty?
20
+ c.quote(nil)
21
+ else
22
+ value.map { |v| c.quote(v) }.join(',')
23
+ end
24
+ else
25
+ c.quote(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,157 @@
1
+ require 'active_record/schema_dumper'
2
+
3
+ module ActiveRecord
4
+ class SchemaDumper
5
+ VALID_COLUMN_SPEC_KEYS = [:name, :limit, :precision, :scale, :default, :null, :array]
6
+ def self.valid_column_spec_keys
7
+ VALID_COLUMN_SPEC_KEYS
8
+ end
9
+
10
+ def dump(stream)
11
+ header(stream)
12
+ # added
13
+ extensions(stream) if @connection.respond_to?(:supports_extensions?) &&
14
+ @connection.supports_extensions?
15
+ # /added
16
+ tables(stream)
17
+ trailer(stream)
18
+ stream
19
+ end
20
+
21
+ private
22
+
23
+ def extensions(stream)
24
+ exts = @connection.extensions
25
+
26
+ unless exts.empty?
27
+ stream.puts exts.map { |name| " add_extension \"#{name}\""}.join("\n") + "\n\n"
28
+ end
29
+ end
30
+
31
+ def table(table, stream)
32
+ columns = @connection.columns(table)
33
+ begin
34
+ tbl = StringIO.new
35
+
36
+ # first dump primary key column
37
+ if @connection.respond_to?(:pk_and_sequence_for)
38
+ pk, _ = @connection.pk_and_sequence_for(table)
39
+ elsif @connection.respond_to?(:primary_key)
40
+ pk = @connection.primary_key(table)
41
+ end
42
+
43
+ tbl.print " create_table #{table.inspect}"
44
+ if columns.detect { |c| c.name == pk }
45
+ if pk != 'id'
46
+ tbl.print %Q(, :primary_key => "#{pk}")
47
+ end
48
+ else
49
+ tbl.print ", :id => false"
50
+ end
51
+ tbl.print ", :force => true"
52
+ tbl.puts " do |t|"
53
+
54
+ # then dump all non-primary key columns
55
+ column_specs = columns.map do |column|
56
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
57
+ next if column.name == pk
58
+ spec = column_spec(column)
59
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
60
+ spec
61
+ end.compact
62
+
63
+ # find all migration keys used in this table
64
+ keys = self.class.valid_column_spec_keys & column_specs.map{ |k| k.keys }.flatten
65
+
66
+ # figure out the lengths for each column based on above keys
67
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
68
+
69
+ # the string we're going to sprintf our values against, with standardized column widths
70
+ format_string = lengths.map{ |len| "%-#{len}s" }
71
+
72
+ # find the max length for the 'type' column, which is special
73
+ type_length = column_specs.map{ |column| column[:type].length }.max
74
+
75
+ # add column type definition to our format string
76
+ format_string.unshift " t.%-#{type_length}s "
77
+
78
+ format_string *= ''
79
+
80
+ column_specs.each do |colspec|
81
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
82
+ values.unshift colspec[:type]
83
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
84
+ tbl.puts
85
+ end
86
+
87
+ tbl.puts " end"
88
+ tbl.puts
89
+
90
+ indexes(table, tbl)
91
+
92
+ tbl.rewind
93
+ stream.print tbl.read
94
+ rescue => e
95
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
96
+ stream.puts "# #{e.message}"
97
+ stream.puts
98
+ end
99
+
100
+ stream
101
+ end
102
+
103
+ #mostly rails 3.2 code
104
+ def indexes(table, stream)
105
+ if (indexes = @connection.indexes(table)).any?
106
+ add_index_statements = indexes.map do |index|
107
+ statement_parts = [
108
+ ('add_index ' + index.table.inspect),
109
+ index.columns.inspect,
110
+ (':name => ' + index.name.inspect),
111
+ ]
112
+ statement_parts << ':unique => true' if index.unique
113
+
114
+ index_lengths = (index.lengths || []).compact
115
+ statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
116
+
117
+ index_orders = (index.orders || {})
118
+ statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?
119
+
120
+ # changed from rails 2.3
121
+ statement_parts << (':where => ' + index.where.inspect) if index.where
122
+ statement_parts << (':using => ' + index.using.inspect) if index.using
123
+ statement_parts << (':index_opclass => ' + index.index_opclass.inspect) if index.index_opclass.present?
124
+ # /changed
125
+
126
+ ' ' + statement_parts.join(', ')
127
+ end
128
+
129
+ stream.puts add_index_statements.sort.join("\n")
130
+ stream.puts
131
+ end
132
+ end
133
+
134
+ #mostly rails 3.2 code (pulled out of table method)
135
+ def column_spec(column)
136
+ spec = {}
137
+ spec[:name] = column.name.inspect
138
+
139
+ # AR has an optimization which handles zero-scale decimals as integers. This
140
+ # code ensures that the dumper still dumps the column as a decimal.
141
+ spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) }
142
+ 'decimal'
143
+ else
144
+ column.type.to_s
145
+ end
146
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal'
147
+ spec[:precision] = column.precision.inspect if column.precision
148
+ spec[:scale] = column.scale.inspect if column.scale
149
+ spec[:null] = 'false' unless column.null
150
+ spec[:default] = default_string(column.default) if column.has_default?
151
+ # changed from rails 3.2 code
152
+ spec[:array] = 'true' if column.respond_to?(:array) && column.array
153
+ # /changed
154
+ spec
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,3 @@
1
+ require 'activerecord-postgis-array/arel/nodes'
2
+ require 'activerecord-postgis-array/arel/predications'
3
+ require 'activerecord-postgis-array/arel/visitors'
@@ -0,0 +1,2 @@
1
+ require 'activerecord-postgis-array/arel/nodes/array_nodes'
2
+ require 'activerecord-postgis-array/arel/nodes/contained_within'
@@ -0,0 +1,9 @@
1
+ require 'arel/nodes/binary'
2
+
3
+ module Arel
4
+ module Nodes
5
+ class Overlap < Arel::Nodes::Binary
6
+ def operator; '&&' end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ require 'arel/nodes/binary'
2
+ module Arel
3
+ module Nodes
4
+ class ContainedWithin < Arel::Nodes::Binary
5
+ def operator; :<< end
6
+ end
7
+
8
+ class ContainedWithinEquals < Arel::Nodes::Binary
9
+ def operator; '<<='.to_sym end
10
+ end
11
+
12
+ class Contains < Arel::Nodes::Binary
13
+ def operator; :>> end
14
+ end
15
+
16
+ class ContainsEquals < Arel::Nodes::Binary
17
+ def operator; '>>='.to_sym end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require 'arel/predications'
2
+
3
+ module Arel
4
+ module Predications
5
+ def contained_within(other)
6
+ Nodes::ContainedWithin.new self, other
7
+ end
8
+
9
+ def contained_within_or_equals(other)
10
+ Nodes::ContainedWithinEquals.new self, other
11
+ end
12
+
13
+ def contains(other)
14
+ Nodes::Contains.new self, other
15
+ end
16
+
17
+ def contains_or_equals(other)
18
+ Nodes::ContainsEquals.new self, other
19
+ end
20
+
21
+ def overlap(other)
22
+ Nodes::Overlap.new self, other
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,2 @@
1
+ require 'activerecord-postgis-array/arel/visitors/visitor'
2
+ require 'activerecord-postgis-array/arel/visitors/to_sql'
@@ -0,0 +1,15 @@
1
+ require 'arel/visitors/to_sql'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class ToSql
6
+ def visit_Array o
7
+ if last_column.respond_to?(:array) && last_column.array
8
+ quoted o
9
+ else
10
+ o.empty? ? 'NULL' : o.map { |x| visit x }.join(', ')
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ require 'arel/visitors/visitor'
2
+ module Arel
3
+ module Visitors
4
+ class Visitor
5
+ # We are adding our visitors to the main visitor for the time being until the right spot is found to monkey patch
6
+ private
7
+ def visit_Arel_Nodes_ContainedWithin o
8
+ "#{visit o.left} << #{visit o.right}"
9
+ end
10
+
11
+ def visit_Arel_Nodes_ContainedWithinEquals o
12
+ "#{visit o.left} <<= #{visit o.right}"
13
+ end
14
+
15
+ def visit_Arel_Nodes_Contains o
16
+ left_column = o.left.relation.engine.columns.find { |col| col.name == o.left.name.to_s }
17
+
18
+ if left_column && left_column.respond_to?(:array) && left_column.array
19
+ "#{visit o.left} @> #{visit o.right}"
20
+ else
21
+ "#{visit o.left} >> #{visit o.right}"
22
+ end
23
+ end
24
+
25
+ def visit_Arel_Nodes_ContainsEquals o
26
+ "#{visit o.left} >>= #{visit o.right}"
27
+ end
28
+
29
+ def visit_Arel_Nodes_Overlap o
30
+ "#{visit o.left} && #{visit o.right}"
31
+ end
32
+
33
+ def visit_IPAddr value
34
+ "'#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}'"
35
+ end
36
+ end
37
+ end
38
+ end