activerecord-postgis-array 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +99 -0
- data/CONTRIBUTING.md +35 -0
- data/Gemfile +12 -0
- data/LICENSE +22 -0
- data/README.md +87 -0
- data/Rakefile +33 -0
- data/activerecord-postgis-array.gemspec +30 -0
- data/docs/indexes.md +28 -0
- data/docs/migrations.md +92 -0
- data/docs/querying.md +170 -0
- data/docs/type_casting.md +51 -0
- data/lib/activerecord-postgis-array.rb +3 -0
- data/lib/activerecord-postgis-array/active_record.rb +4 -0
- data/lib/activerecord-postgis-array/active_record/connection_adapters.rb +1 -0
- data/lib/activerecord-postgis-array/active_record/connection_adapters/postgres_adapter.rb +346 -0
- data/lib/activerecord-postgis-array/active_record/relation.rb +2 -0
- data/lib/activerecord-postgis-array/active_record/relation/predicate_builder.rb +71 -0
- data/lib/activerecord-postgis-array/active_record/relation/query_methods.rb +84 -0
- data/lib/activerecord-postgis-array/active_record/sanitization.rb +30 -0
- data/lib/activerecord-postgis-array/active_record/schema_dumper.rb +157 -0
- data/lib/activerecord-postgis-array/arel.rb +3 -0
- data/lib/activerecord-postgis-array/arel/nodes.rb +2 -0
- data/lib/activerecord-postgis-array/arel/nodes/array_nodes.rb +9 -0
- data/lib/activerecord-postgis-array/arel/nodes/contained_within.rb +20 -0
- data/lib/activerecord-postgis-array/arel/predications.rb +25 -0
- data/lib/activerecord-postgis-array/arel/visitors.rb +2 -0
- data/lib/activerecord-postgis-array/arel/visitors/to_sql.rb +15 -0
- data/lib/activerecord-postgis-array/arel/visitors/visitor.rb +38 -0
- data/lib/activerecord-postgis-array/version.rb +3 -0
- data/spec/arel/arel_spec.rb +30 -0
- data/spec/arel/array_spec.rb +77 -0
- data/spec/columns/array_spec.rb +120 -0
- data/spec/dummy/.gitignore +15 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/images/rails.png +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/person.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +59 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml.example +14 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +38 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/migrate/20120501163758_create_people.rb +12 -0
- data/spec/dummy/db/schema.rb +25 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/lib/tasks/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/index.html +241 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/factories/people.rb +7 -0
- data/spec/dummy/test/fixtures/.gitkeep +0 -0
- data/spec/dummy/test/functional/.gitkeep +0 -0
- data/spec/dummy/test/integration/.gitkeep +0 -0
- data/spec/dummy/test/performance/browsing_test.rb +12 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/dummy/test/unit/.gitkeep +0 -0
- data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
- data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
- data/spec/dummy/vendor/plugins/.gitkeep +0 -0
- data/spec/migrations/active_record_migration_spec.rb +29 -0
- data/spec/migrations/array_spec.rb +136 -0
- data/spec/migrations/index_spec.rb +67 -0
- data/spec/models/array_spec.rb +285 -0
- data/spec/queries/array_queries_spec.rb +72 -0
- data/spec/queries/sanity_spec.rb +16 -0
- data/spec/schema_dumper/array_spec.rb +17 -0
- data/spec/schema_dumper/extension_spec.rb +14 -0
- data/spec/schema_dumper/index_spec.rb +46 -0
- data/spec/spec_helper.rb +29 -0
- 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 @@
|
|
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
|