activerecord-postgis-array 0.3.4

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