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
data/docs/querying.md ADDED
@@ -0,0 +1,170 @@
1
+ # Querying PostgreSQL datatypes
2
+
3
+ * [Arrays](#arrays)
4
+ * [INET/CIDR](#inetcidr-queries)
5
+
6
+ ## Arrays
7
+
8
+ * [&& - Array Overlap operator](#---array-overlap-operator)
9
+ * [ANY or ALL functions](#any-or-all-functions)
10
+
11
+ ### && - Array Overlap operator
12
+
13
+ PostgreSQL implements the `&&` operator, known as the overlap operator,
14
+ for arrays. The overlap operator returns `t` (true) when two arrays have
15
+ one or more elements in common.
16
+
17
+ ```sql
18
+ ARRAY[1,2,3] && ARRAY[4,5,6]
19
+ -- f
20
+
21
+ ARRAY[1,2,3] && ARRAY[3,5,6]
22
+ -- t
23
+ ```
24
+
25
+ Postgres\_ext extends the `ActiveRecord::Relation.where` method similar
26
+ to the Rails 4.0 not clause. The easiest way to make a overlap query
27
+ would be:
28
+
29
+ ```ruby
30
+ User.where.overlap(:nick_names => ['Bob', 'Fred'])
31
+ ```
32
+
33
+ Postgres\_ext defines `array_overlap`, an [Arel](https://github.com/rails/arel)
34
+ predicate for the `&&` operator. This is utilized by the `where.overlap`
35
+ call above.
36
+
37
+ ```ruby
38
+ user_arel = User.arel_table
39
+
40
+ # Execute the query
41
+ User.where(user_arel[:tags].array_overlap(['one','two']))
42
+ # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"tags\" && '{one,two}'
43
+ ```
44
+
45
+ ### @> - Array Contains operator
46
+
47
+ PostgreSQL has a contains (`@>`) operator for querying whether all the
48
+ elements of an array are within another.
49
+
50
+ ```sql
51
+ ARRAY[1,2,3] @> ARRAY[3,4]
52
+ -- f
53
+
54
+ ARRAY[1,2,3] @> ARRAY[2,3]
55
+ -- t
56
+ ```
57
+
58
+ Postgres\_ext extends the `ActiveRecord::Relation.where` method by
59
+ adding a `contains` method. To make a contains query, you can do:
60
+
61
+ ```ruby
62
+ User.where.contains(:nick_names => ['Bob', 'Fred'])
63
+ ```
64
+
65
+ Postgres\_ext defines `array_contains`, an [Arel](https://github.com/rails/arel)
66
+ predicate for the `@>` operator. This is utilized by the
67
+ `where.array_contains` call above.
68
+
69
+ ```ruby
70
+ user_arel = User.arel_table
71
+
72
+ # Execute the query
73
+ User.where(user_arel[:tags].array_contains(['one','two']))
74
+ # => SELECT "users".* FROM "users" WHERE "users"."tags" @> '{"one","two"}'
75
+ ```
76
+
77
+ ### ANY or ALL functions
78
+
79
+ When querying array columns, you have the ability to see if a predicate
80
+ apply's to either *any* element in the array, or *all* elements of the
81
+ array. The syntax for these predicates are slightly different then the
82
+ normal `where` syntax in PostgreSQL. To see if an array contains the
83
+ string `'test'` in any location, you would write the following in SQL
84
+
85
+ ```sql
86
+ SELECT *
87
+ FROM users
88
+ WHERE 'test' = ANY(users.tags)
89
+ ```
90
+
91
+ Notice that the column is on the right hand side of the predicate,
92
+ instead of the left, because we have to call the `ANY` function on that
93
+ column.
94
+
95
+ Postgres\_ext provides a `ActiveRecord::Relation.where.any()` method. The
96
+ easiest way to make a ANY query would be:
97
+
98
+ ```ruby
99
+ User.where.any(:nick_names => 'Bob')
100
+ ```
101
+
102
+ There is also an `ActiveRecord::Relation.where.all()` call as well. This
103
+ method utilizes the following code to create the query:
104
+
105
+ We can generate the above query using [Arel](https://github.com/rails/arel)
106
+ and generating the Node manually. We would use the following to
107
+ accompish this:
108
+
109
+ ```ruby
110
+ user_arel = User.arel_table
111
+
112
+ any_tags_function = Arel::Nodes::NamedFunction.new('ANY', [user_arel[:tags]])
113
+ predicate = Arel::Nodes::Equality.new('test', any_tags_function)
114
+
115
+ # Execute the query
116
+ User.where(predicate)
117
+ #=> SELECT \"users\".* FROM \"users\" WHERE 'test' = ANY(\"users\".\"tags\")
118
+ ```
119
+
120
+ The ALL version of this same predicate can be generated by swap `'ANY'`
121
+ for `'ALL'` in the named function.
122
+
123
+ ## INET/CIDR Queries
124
+
125
+ PostgreSQL defines the `<<`, or contained within operator for INET and
126
+ CIDR datatypes. The `<<` operator returns `t` (true) if a INET or CIDR
127
+ address is contained within the given subnet.
128
+
129
+ ```sql
130
+ inet '192.168.1.6' << inet '10.0.0.0/24'
131
+ -- f
132
+
133
+ inet '192.168.1.6' << inet '192.168.1.0/24'
134
+ -- t
135
+ ```
136
+
137
+ In addition to contained within, there is also:
138
+
139
+ * `<<=` - Contained within or equals
140
+ * `>>` - Contains
141
+ * `>>=` - Contains or equals
142
+
143
+ Postgres\_ext extends the `ActiveRecord::Relation.where` method similar
144
+ to the Rails 4.0 not clause. The easiest way to make a overlap query
145
+ would be:
146
+
147
+ ```ruby
148
+ User.where.contained_within(:ip => '192.168.1.1/24')
149
+ User.where.contained_within_or_equals(:ip => '192.168.1.1/24')
150
+ User.where.contains(:ip => '192.168.1.14')
151
+ User.where.contains_or_equals(:ip => '192.168.1.14')
152
+ ```
153
+
154
+ Postgres\_ext defines `contained_within`, an [Arel](https://github.com/rails/arel)
155
+ predicate for the `<<` operator. This is utilized by the
156
+ methods above.
157
+
158
+ ```ruby
159
+ user_arel = User.arel_table
160
+
161
+ # Execute the query
162
+ User.where(user_arel[:ip_address].contained_within('127.0.0.1/24'))
163
+ # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip_address\" << '127.0.0.1/24'
164
+ User.where(user_arel[:ip_address].contained_within_or_equals('127.0.0.1/24'))
165
+ # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip_address\" <<= '127.0.0.1/24'
166
+ User.where(user_arel[:ip_address].contains('127.0.0.1'))
167
+ # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip_address\" >> '127.0.0.1'
168
+ User.where(user_arel[:ip_address].contains_or_equals('127.0.0.1'))
169
+ # => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip_address\" >>= '127.0.0.1'
170
+ ```
@@ -0,0 +1,51 @@
1
+ # Type Casting support
2
+
3
+ ## INET and CIDR
4
+ INET and CIDR values are converted to
5
+ [IPAddr](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ipaddr/rdoc/IPAddr.html)
6
+ objects when retrieved from the database, or set as a string.
7
+
8
+ ```ruby
9
+ create_table :inet_examples do |t|
10
+ t.inet :ip_address
11
+ end
12
+
13
+ class InetExample < ActiveRecord::Base
14
+ end
15
+
16
+ inetExample = InetExample.new
17
+ inetExample.ip_address = '127.0.0.0/24'
18
+ inetExample.ip_address
19
+ # => #<IPAddr: IPv4:127.0.0.0/255.255.255.0>
20
+ inetExample.save
21
+
22
+ inet_2 = InetExample.first
23
+ inet_2.ip_address
24
+ # => #<IPAddr: IPv4:127.0.0.0/255.255.255.0>
25
+ ```
26
+
27
+ ## Arrays
28
+ Array values can be set with Array objects. Any array stored in the
29
+ database will be converted to a properly casted array of values on the
30
+ way out.
31
+
32
+ ```ruby
33
+ create_table :people do |t|
34
+ t.integer :favorite_numbers, :array => true
35
+ end
36
+
37
+ class Person < ActiveRecord::Base
38
+ end
39
+
40
+ person = Person.new
41
+ person.favorite_numbers = [1,2,3]
42
+ person.favorite_numbers
43
+ # => [1,2,3]
44
+ person.save
45
+
46
+ person_2 = Person.first
47
+ person_2.favorite_numbers
48
+ # => [1,2,3]
49
+ person_2.favorite_numbers.first.class
50
+ # => Fixnum
51
+ ```
@@ -0,0 +1,3 @@
1
+ require 'activerecord-postgis-array/version'
2
+ require 'activerecord-postgis-array/active_record'
3
+ require 'activerecord-postgis-array/arel'
@@ -0,0 +1,4 @@
1
+ require 'activerecord-postgis-array/active_record/sanitization'
2
+ require 'activerecord-postgis-array/active_record/connection_adapters'
3
+ require 'activerecord-postgis-array/active_record/schema_dumper'
4
+ require 'activerecord-postgis-array/active_record/relation'
@@ -0,0 +1 @@
1
+ require 'activerecord-postgis-array/active_record/connection_adapters/postgres_adapter'
@@ -0,0 +1,346 @@
1
+ require 'activerecord-postgis-adapter'
2
+ require 'ipaddr'
3
+ require 'pg_array_parser'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module PostGISAdapter
8
+ ::RGeo::ActiveRecord::SpatialIndexDefinition.class_eval do
9
+ attr_accessor :using, :index_opclass
10
+ end
11
+
12
+ SpatialColumn.class_eval do
13
+ include PgArrayParser
14
+ attr_accessor :array
15
+
16
+ def initialize_with_extended_types(name, default, sql_type = nil, null = true, opts = {})
17
+ if sql_type =~ /\[\]$/
18
+ @array = true
19
+ initialize_without_extended_types(name, default, sql_type[0..sql_type.length - 3], null, opts)
20
+ @sql_type = sql_type
21
+ else
22
+ initialize_without_extended_types(name,default, sql_type, null, opts)
23
+ end
24
+ end
25
+ alias_method_chain :initialize, :extended_types
26
+
27
+ def type_cast_with_extended_types(value)
28
+ return nil if value.nil?
29
+ return coder.load(value) if encoded?
30
+
31
+ klass = self.class
32
+ if self.array && String === value && value.start_with?('{') && value.end_with?('}')
33
+ string_to_array value
34
+ elsif self.array && Array === value
35
+ value
36
+ else
37
+ type_cast_without_extended_types(value)
38
+ end
39
+ end
40
+ alias_method_chain :type_cast, :extended_types
41
+
42
+ def string_to_array(value)
43
+ if Array === value
44
+ value
45
+ else
46
+ string_array = parse_pg_array value
47
+ if type == :string || type == :text
48
+ force_character_encoding(string_array)
49
+ else
50
+ type_cast_array(string_array)
51
+ end
52
+ end
53
+ end
54
+
55
+ def type_cast_array(array)
56
+ array.map do |value|
57
+ Array === value ? type_cast_array(value) : type_cast(value)
58
+ end
59
+ end
60
+
61
+ def number?
62
+ !self.array && super
63
+ end
64
+
65
+ def type_cast_code_with_extended_types(var_name)
66
+ klass = self.class.name
67
+
68
+ if self.array
69
+ "#{klass}.new('#{self.name}', #{self.default.nil? ? 'nil' : "'#{self.default}'"}, '#{self.sql_type}').string_to_array(#{var_name})"
70
+ else
71
+ type_cast_code_without_extended_types(var_name)
72
+ end
73
+ end
74
+ alias_method_chain :type_cast_code, :extended_types
75
+
76
+ private
77
+
78
+ def force_character_encoding(string_array)
79
+ string_array.map do |item|
80
+ item.respond_to?(:force_encoding) ? item.force_encoding(ActiveRecord::Base.connection.encoding_for_ruby) : item
81
+ end
82
+ end
83
+ end
84
+
85
+ MainAdapter.class_eval do
86
+ class UnsupportedFeature < Exception; end
87
+
88
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
89
+ attr_accessor :array
90
+ end
91
+
92
+ SpatialTableDefinition.class_eval do
93
+
94
+ def column(name_, type_, options_={})
95
+ if (info_ = @base.spatial_column_constructor(type_.to_sym))
96
+ type_ = options_[:type] || info_[:type] || type_
97
+ if type_.to_s == 'geometry' &&
98
+ (options_[:no_constraints] ||
99
+ options_[:limit].is_a?(::Hash) && options_[:limit][:no_constraints])
100
+ then
101
+ options_.delete(:limit)
102
+ else
103
+ options_[:type] = type_
104
+ type_ = :spatial
105
+ end
106
+ end
107
+ super(name_, type_, options_)
108
+ if type_ == :spatial
109
+ col_ = self[name_]
110
+ col_.extend(SpatialColumnDefinitionMethods) unless col_.respond_to?(:geographic?)
111
+ options_.merge!(col_.limit) if col_.limit.is_a?(::Hash)
112
+ col_.set_spatial_type(options_[:type])
113
+ col_.set_geographic(options_[:geographic])
114
+ col_.set_srid(options_[:srid])
115
+ col_.set_has_z(options_[:has_z])
116
+ col_.set_has_m(options_[:has_m])
117
+ end
118
+
119
+ column = self[name_]
120
+ column.array = options_[:array]
121
+
122
+ self
123
+ end
124
+
125
+ private
126
+
127
+ def new_column_definition(base, name, type)
128
+ definition = ColumnDefinition.new base, name, type
129
+ @columns << definition
130
+ @columns_hash[name] = definition
131
+ definition
132
+ end
133
+ end
134
+
135
+ # Translate from the current database encoding to the encoding we
136
+ # will force string array components into on retrievial.
137
+ def encoding_for_ruby
138
+ @database_encoding ||= case ActiveRecord::Base.connection.encoding
139
+ when 'UTF8'
140
+ 'UTF-8'
141
+ else
142
+ ActiveRecord::Base.connection.encoding
143
+ end
144
+ end
145
+
146
+ def supports_extensions?
147
+ postgresql_version > 90100
148
+ end
149
+
150
+ def add_column_options!(sql, options)
151
+ if options[:array] || options[:column].try(:array)
152
+ sql << '[]'
153
+ end
154
+ super
155
+ end
156
+
157
+ def add_index(table_name, column_name, options = {})
158
+ index_name, unique, index_columns, _ = add_index_options(table_name, column_name, options)
159
+ if options.is_a? Hash
160
+ index_type = options[:using] ? " USING #{options[:using]} " : ""
161
+ index_type = 'USING GIST' if options[:spatial]
162
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
163
+ index_opclass = options[:index_opclass]
164
+ index_algorithm = options[:algorithm] == :concurrently ? ' CONCURRENTLY' : ''
165
+
166
+ if options[:algorithm].present? && options[:algorithm] != :concurrently
167
+ raise ArgumentError.new 'Algorithm must be one of the following: :concurrently'
168
+ end
169
+ end
170
+ execute "CREATE #{unique} INDEX#{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}#{index_type}(#{index_columns} #{index_opclass})#{index_options}"
171
+ end
172
+
173
+ def add_extension(extension_name, options={})
174
+ raise UnsupportedFeature.new('Extensions are not support by this version of PostgreSQL') unless supports_extensions?
175
+ execute "CREATE extension IF NOT EXISTS \"#{extension_name}\""
176
+ end
177
+
178
+ def change_table(table_name, options = {})
179
+ if supports_bulk_alter? && options[:bulk]
180
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
181
+ yield Table.new(table_name, recorder)
182
+ bulk_change_table(table_name, recorder.commands)
183
+ else
184
+ yield Table.new(table_name, self)
185
+ end
186
+ end
187
+
188
+ if RUBY_PLATFORM =~ /java/
189
+ # The activerecord-jbdc-adapter implements PostgreSQLAdapter#add_column differently from the active-record version
190
+ # so we have to patch that version in JRuby, but not in MRI/YARV
191
+ def add_column(table_name, column_name, type, options = {})
192
+ default = options[:default]
193
+ notnull = options[:null] == false
194
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
195
+
196
+ if options[:array]
197
+ sql_type << '[]'
198
+ end
199
+
200
+ # Add the column.
201
+ execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{sql_type}")
202
+
203
+ change_column_default(table_name, column_name, default) if options_include_default?(options)
204
+ change_column_null(table_name, column_name, false, default) if notnull
205
+ end
206
+ end
207
+
208
+ def type_cast_extended(value, column, part_array = false)
209
+ case value
210
+ when NilClass
211
+ if column.array && part_array
212
+ 'NULL'
213
+ elsif column.array && !part_array
214
+ value
215
+ else
216
+ type_cast_without_extended_types(value, column)
217
+ end
218
+ when Array
219
+ if column.array
220
+ array_to_string(value, column)
221
+ else
222
+ type_cast_without_extended_types(value, column)
223
+ end
224
+ else
225
+ type_cast_without_extended_types(value, column)
226
+ end
227
+ end
228
+
229
+ def type_cast_with_extended_types(value, column)
230
+ type_cast_extended(value, column)
231
+ end
232
+ alias_method_chain :type_cast, :extended_types
233
+
234
+ def quote_with_extended_types(value, column = nil)
235
+ if value.is_a? Array
236
+ "'#{array_to_string(value, column, true)}'"
237
+ elsif column.respond_to?(:array) && column.array && value =~ /^\{.*\}$/
238
+ "'#{value}'"
239
+ else
240
+ quote_without_extended_types(value, column)
241
+ end
242
+ end
243
+ alias_method_chain :quote, :extended_types
244
+
245
+ def opclasses
246
+ @opclasses ||= select_rows('SELECT opcname FROM pg_opclass').flatten.uniq
247
+ end
248
+
249
+ # this is based upon rails 4 changes to include different index methods
250
+ # Returns an array of indexes for the given table.
251
+ def indexes(table_name_, name_ = nil)
252
+ opclasses
253
+ # FULL REPLACEMENT. RE-CHECK ON NEW VERSIONS.
254
+ result_ = query(<<-SQL, 'SCHEMA')
255
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
256
+ FROM pg_class t
257
+ INNER JOIN pg_index d ON t.oid = d.indrelid
258
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
259
+ WHERE i.relkind = 'i'
260
+ AND d.indisprimary = 'f'
261
+ AND t.relname = '#{table_name_}'
262
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
263
+ ORDER BY i.relname
264
+ SQL
265
+
266
+ result_.map do |row_|
267
+ index_name_ = row_[0]
268
+ unique_ = row_[1] == 't'
269
+ indkey_ = row_[2].split(" ")
270
+ inddef_ = row_[3]
271
+ oid_ = row_[4]
272
+
273
+ columns_ = query(<<-SQL, "SCHEMA")
274
+ SELECT a.attnum, a.attname, t.typname
275
+ FROM pg_attribute a, pg_type t
276
+ WHERE a.attrelid = #{oid_}
277
+ AND a.attnum IN (#{indkey_.join(",")})
278
+ AND a.atttypid = t.oid
279
+ SQL
280
+ columns_ = columns_.inject({}){ |h_, r_| h_[r_[0].to_s] = [r_[1], r_[2]]; h_ }
281
+ column_names_ = columns_.values_at(*indkey_).compact.map{ |a_| a_[0] }
282
+
283
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
284
+ desc_order_columns_ = inddef_.scan(/(\w+) DESC/).flatten
285
+ orders_ = desc_order_columns_.any? ? Hash[desc_order_columns_.map {|order_column_| [order_column_, :desc]}] : {}
286
+ where_ = inddef_.scan(/WHERE (.+)$/).flatten[0]
287
+ spatial_ = inddef_ =~ /using\s+gist/i && columns_.size == 1 &&
288
+ (columns_.values.first[1] == 'geometry' || columns_.values.first[1] == 'geography')
289
+
290
+ using_ = inddef_.scan(/USING (.+?) /).flatten[0].to_sym
291
+ if using_
292
+ index_op_ = inddef_.scan(/USING .+? \(.+? (#{opclasses.join('|')})\)/).flatten
293
+ index_op_ = index_op_[0].to_sym if index_op_.present?
294
+ end
295
+
296
+ if column_names_.present?
297
+ index_def_ = ::RGeo::ActiveRecord::SpatialIndexDefinition.new(table_name_, index_name_, unique_, column_names_, [], orders_, where_, spatial_ ? true : false)
298
+ index_def_.using = using_ if using_ && using_ != :btree && !spatial_
299
+ index_def_.index_opclass = index_op_ if using_ && using_ != :btree && !spatial_ && index_op_
300
+ index_def_
301
+ else
302
+ nil
303
+ end
304
+ #/changed
305
+ end.compact
306
+ end
307
+
308
+ def extensions
309
+ select_rows('select extname from pg_extension', 'extensions').map { |row| row[0] }.delete_if {|name| name == 'plpgsql'}
310
+ end
311
+
312
+ private
313
+
314
+ def array_to_string(value, column, encode_single_quotes = false)
315
+ "{#{value.map { |val| item_to_string(val, column, encode_single_quotes) }.join(',')}}"
316
+ end
317
+
318
+ def item_to_string(value, column, encode_single_quotes = false)
319
+ return 'NULL' if value.nil?
320
+
321
+ casted_value = type_cast_extended(value, column, true)
322
+
323
+ if casted_value.is_a?(String) && value.is_a?(String)
324
+ casted_value = casted_value.dup
325
+ # Encode backslashes. One backslash becomes 4 in the resulting SQL.
326
+ # (why 4, and not 2? Trial and error shows 4 works, 2 fails to parse.)
327
+ casted_value.gsub!('\\', '\\\\\\\\')
328
+ # Encode a bare " in the string as \"
329
+ casted_value.gsub!('"', '\\"')
330
+ # PostgreSQL parses the string values differently if they are quoted for
331
+ # use in a statement, or if it will be used as part of a bound argument.
332
+ # For directly-inserted values (UPDATE foo SET bar='{"array"}') we need to
333
+ # escape ' as ''. For bound arguments, do not escape them.
334
+ if encode_single_quotes
335
+ casted_value.gsub!("'", "''")
336
+ end
337
+
338
+ "\"#{casted_value}\""
339
+ else
340
+ casted_value
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end