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.
- 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
|