activerecord-postgresql-extensions 0.1.0 → 0.2.0

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.
@@ -0,0 +1,46 @@
1
+
2
+ module ActiveRecord
3
+ module PostgreSQLExtensions
4
+ class FeatureNotSupportedError < Exception
5
+ def initialize(feature)
6
+ super(%{The feature "#{feature}" is not supported by server. (Server version #{ActiveRecord::PostgreSQLExtensions.SERVER_VERSION}.)"})
7
+ end
8
+ end
9
+
10
+ module Features
11
+ class << self
12
+ def extensions?
13
+ if defined?(@has_extensions)
14
+ @has_extensions
15
+ else
16
+ @has_extensions = ActiveRecord::PostgreSQLExtensions.SERVER_VERSION >= '9.1'
17
+ end
18
+ end
19
+
20
+ def foreign_tables?
21
+ if defined?(@has_foreign_tables)
22
+ @has_foreign_tables
23
+ else
24
+ @has_foreign_tables = ActiveRecord::PostgreSQLExtensions.SERVER_VERSION >= '9.1'
25
+ end
26
+ end
27
+
28
+ def modify_mass_privileges?
29
+ if defined?(@has_modify_mass_privileges)
30
+ @has_modify_mass_privileges
31
+ else
32
+ @has_modify_mass_privileges = ActiveRecord::PostgreSQLExtensions.SERVER_VERSION >= '9.0'
33
+ end
34
+ end
35
+
36
+ def postgis?
37
+ if defined?(@has_postgis)
38
+ @has_postgis
39
+ else
40
+ @has_postgis = !!ActiveRecord::PostgreSQLExtensions::PostGIS.VERSION
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -117,12 +117,28 @@ module ActiveRecord
117
117
  # name with drop_index. See create_index for the particulars on
118
118
  # why.
119
119
  #
120
+ # You can specify multiple INDEXes with an Array when using drop_index,
121
+ # but you may need to use the method directly through the ActiveRecord
122
+ # connection rather than the Migration method, as the Migration method
123
+ # likes to escape the Array to a String.
124
+ #
120
125
  # ==== Options
121
126
  #
122
127
  # * <tt>:if_exists</tt> - adds IF EXISTS.
123
128
  # * <tt>:cascade</tt> - adds CASCADE.
129
+ # * <tt>:concurrently</tt> - adds the CONCURRENTLY option when dropping
130
+ # the INDEX. When using the :concurrently option, only one INDEX can
131
+ # specified and the :cascade option cannot be used. See the PostgreSQL
132
+ # documentation for details.
124
133
  def drop_index(name, options = {})
134
+ if options[:concurrently] && options[:cascade]
135
+ raise ArgumentError.new("The :concurrently and :cascade options cannot be used together.")
136
+ elsif options[:concurrently] && name.is_a?(Array) && name.length > 1
137
+ raise ArgumentError.new("The :concurrently option can only be used on a single INDEX.")
138
+ end
139
+
125
140
  sql = 'DROP INDEX '
141
+ sql << 'CONCURRENTLY ' if options[:concurrently]
126
142
  sql << 'IF EXISTS ' if options[:if_exists]
127
143
  sql << Array(name).collect { |i| quote_generic(i) }.join(', ')
128
144
  sql << ' CASCADE' if options[:cascade]
@@ -200,7 +200,9 @@ module ActiveRecord
200
200
  #
201
201
  # When using the grant_*_privileges methods, you can specify multiple
202
202
  # permissions, objects and roles by using Arrays for the appropriate
203
- # argument.
203
+ # argument. You can also apply the privileges to all objects within a
204
+ # schema by using the :all option in the options Hash and supply the schema
205
+ # name as the first argument.
204
206
  #
205
207
  # ==== Examples
206
208
  #
@@ -210,6 +212,9 @@ module ActiveRecord
210
212
  # grant_sequence_privileges(:my_seq, [ :select, :update ], :public)
211
213
  # # => GRANT SELECT, UPDATE ON SEQUENCE "my_seq" TO PUBLIC
212
214
  #
215
+ # grant_sequence_privileges(:public, [ :select, :update ], :joe, :all => true)
216
+ # # => GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA PUBLIC TO "joe"
217
+ #
213
218
  # You can specify the <tt>:with_grant_option</tt> in any of the
214
219
  # grant_*_privilege methods to add a WITH GRANT OPTION clause to
215
220
  # the command.
@@ -219,19 +224,28 @@ module ActiveRecord
219
224
  :quote_objects => true
220
225
  }.merge query_options
221
226
 
222
- sql = "GRANT #{Array(privileges).collect(&:to_s).collect(&:upcase).join(', ')} ON #{type.to_s.upcase} "
227
+ sql = "GRANT #{Array(privileges).collect(&:to_s).collect(&:upcase).join(', ')} ON "
228
+
229
+ if options[:all]
230
+ if !ActiveRecord::PostgreSQLExtensions::Features.modify_mass_privileges?
231
+ raise ActiveRecord::PostgreSQLExtensions::FeatureNotSupportedError.new('modify mass privileges')
232
+ end
223
233
 
224
- sql << Array(objects).collect do |t|
225
- if my_query_options[:quote_objects]
226
- if my_query_options[:ignore_schema]
227
- base.quote_generic_ignore_scoped_schema(t)
234
+ sql << "ALL #{type.to_s.upcase}S IN SCHEMA #{base.quote_schema(objects)}"
235
+ else
236
+ sql << "#{type.to_s.upcase} "
237
+ sql << Array(objects).collect do |t|
238
+ if my_query_options[:quote_objects]
239
+ if my_query_options[:ignore_schema]
240
+ base.quote_generic_ignore_scoped_schema(t)
241
+ else
242
+ base.quote_table_name(t)
243
+ end
228
244
  else
229
- base.quote_table_name(t)
245
+ t
230
246
  end
231
- else
232
- t
233
- end
234
- end.join(', ')
247
+ end.join(', ')
248
+ end
235
249
 
236
250
  sql << ' TO ' << Array(roles).collect do |r|
237
251
  r = r.to_s
@@ -259,7 +273,9 @@ module ActiveRecord
259
273
  #
260
274
  # When using the revoke_*_privileges methods, you can specify multiple
261
275
  # permissions, objects and roles by using Arrays for the appropriate
262
- # argument.
276
+ # argument. You can also apply the privileges to all objects within a
277
+ # schema by using the :all option in the options Hash and supply the schema
278
+ # name as the first argument.
263
279
  #
264
280
  # ==== Examples
265
281
  #
@@ -288,19 +304,28 @@ module ActiveRecord
288
304
 
289
305
  sql = 'REVOKE '
290
306
  sql << 'GRANT OPTION FOR ' if options[:grant_option_for]
291
- sql << "#{Array(privileges).collect(&:to_s).collect(&:upcase).join(', ')} ON #{type.to_s.upcase} "
307
+ sql << "#{Array(privileges).collect(&:to_s).collect(&:upcase).join(', ')} ON "
292
308
 
293
- sql << Array(objects).collect do |t|
294
- if my_query_options[:quote_objects]
295
- if my_query_options[:ignore_schema]
296
- base.quote_generic_ignore_scoped_schema(t)
309
+ if options[:all]
310
+ if !ActiveRecord::PostgreSQLExtensions::Features.modify_mass_privileges?
311
+ raise ActiveRecord::PostgreSQLExtensions::FeatureNotSupportedError.new('modify mass privileges')
312
+ end
313
+
314
+ sql << "ALL #{type.to_s.upcase}S IN SCHEMA #{base.quote_schema(objects)}"
315
+ else
316
+ sql << "#{type.to_s.upcase} "
317
+ sql << Array(objects).collect do |t|
318
+ if my_query_options[:quote_objects]
319
+ if my_query_options[:ignore_schema]
320
+ base.quote_generic_ignore_scoped_schema(t)
321
+ else
322
+ base.quote_table_name(t)
323
+ end
297
324
  else
298
- base.quote_table_name(t)
325
+ t
299
326
  end
300
- else
301
- t
302
- end
303
- end.join(', ')
327
+ end.join(', ')
328
+ end
304
329
 
305
330
  sql << ' FROM ' << Array(roles).collect do |r|
306
331
  r = r.to_s
@@ -29,7 +29,9 @@ module ActiveRecord
29
29
  def UNKNOWN_SRIDS
30
30
  return @UNKNOWN_SRIDS if defined?(@UNKNOWN_SRIDS)
31
31
 
32
- @UNKNOWN_SRIDS = if self.VERSION[:lib] >= '2.0'
32
+ @UNKNOWN_SRIDS = if !self.VERSION
33
+ nil
34
+ elsif self.VERSION[:lib] >= '2.0'
33
35
  {
34
36
  :geography => 0,
35
37
  :geometry => 0
@@ -45,7 +47,9 @@ module ActiveRecord
45
47
  def UNKNOWN_SRID
46
48
  return @UNKNOWN_SRID if defined?(@UNKNOWN_SRID)
47
49
 
48
- @UNKNOWN_SRID = self.UNKNOWN_SRIDS[:geometry]
50
+ @UNKNOWN_SRID = if self.UNKNOWN_SRIDS
51
+ self.UNKNOWN_SRIDS[:geometry]
52
+ end
49
53
  end
50
54
  end
51
55
  end
@@ -178,27 +178,48 @@ module ActiveRecord
178
178
  @table_constraints = Array.new
179
179
  @table_name, @options = table_name, options
180
180
  super(base)
181
-
182
- self.primary_key(
183
- options[:primary_key] || Base.get_primary_key(table_name)
184
- ) unless options[:id] == false
185
181
  end
186
182
 
187
183
  def to_sql #:nodoc:
184
+ if self.options[:of_type]
185
+ if !@columns.empty?
186
+ raise ArgumentError.new("Cannot specify columns while using the :of_type option")
187
+ elsif options[:like]
188
+ raise ArgumentError.new("Cannot specify both the :like and :of_type options")
189
+ elsif options[:inherits]
190
+ raise ArgumentError.new("Cannot specify both the :inherits and :of_type options")
191
+ else
192
+ options[:id] = false
193
+ end
194
+ end
195
+
196
+ unless options[:id] == false
197
+ self.primary_key(options[:primary_key] || Base.get_primary_key(table_name))
198
+
199
+ # ensures that the primary key column is first.
200
+ @columns.unshift(@columns.pop)
201
+ end
202
+
188
203
  sql = 'CREATE '
189
204
  sql << 'TEMPORARY ' if options[:temporary]
190
205
  sql << 'UNLOGGED ' if options[:unlogged]
191
206
  sql << 'TABLE '
192
207
  sql << 'IF NOT EXISTS ' if options[:if_not_exists]
193
- sql << "#{base.quote_table_name(table_name)} "
194
- sql << "OF #{base.quote_table_name(options[:of_type])} " if options[:of_type]
195
- sql << "(\n "
208
+ sql << "#{base.quote_table_name(table_name)}"
209
+ sql << " OF #{base.quote_table_name(options[:of_type])}" if options[:of_type]
196
210
 
197
- ary = @columns.collect(&:to_sql)
198
- ary << @like if defined?(@like) && @like
211
+ ary = []
212
+ if !options[:of_type]
213
+ ary << @columns.collect(&:to_sql)
214
+ ary << @like if defined?(@like) && @like
215
+ end
199
216
  ary << @table_constraints unless @table_constraints.empty?
200
- sql << ary * ",\n "
201
- sql << "\n)"
217
+
218
+ unless ary.empty?
219
+ sql << " (\n "
220
+ sql << ary * ",\n "
221
+ sql << "\n)"
222
+ end
202
223
 
203
224
  sql << "\nINHERITS (" << Array(options[:inherits]).collect { |i| base.quote_table_name(i) }.join(', ') << ')' if options[:inherits]
204
225
  sql << "\nON COMMIT #{options[:on_commit].to_s.upcase.gsub(/_/, ' ')}" if options[:on_commit]
@@ -265,11 +286,16 @@ module ActiveRecord
265
286
  @table_constraints << PostgreSQLExcludeConstraint.new(@base, table_name, excludes, options)
266
287
  end
267
288
 
289
+ def primary_key_constraint(columns, options = {})
290
+ @table_constraints << PostgreSQLPrimaryKeyConstraint.new(@base, columns, options)
291
+ end
292
+
268
293
  def column_with_constraints(name, type, *args) #:nodoc:
269
294
  options = args.extract_options!
270
295
  check = options.delete(:check)
271
296
  references = options.delete(:references)
272
297
  unique = options.delete(:unique)
298
+ primary_key = options.delete(:primary_key)
273
299
  column_without_constraints(name, type, options)
274
300
 
275
301
  if check
@@ -309,6 +335,14 @@ module ActiveRecord
309
335
  end
310
336
  @table_constraints << PostgreSQLUniqueConstraint.new(@base, name, unique)
311
337
  end
338
+
339
+ if primary_key
340
+ unless primary_key.is_a?(Hash)
341
+ primary_key = {}
342
+ end
343
+ @table_constraints << PostgreSQLPrimaryKeyConstraint.new(@base, name, primary_key)
344
+ end
345
+
312
346
  self
313
347
  end
314
348
  alias_method_chain :column, :constraints
@@ -0,0 +1,101 @@
1
+
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ class PostgreSQLAdapter
7
+ # VACUUMs a database, table or columns on a table. See
8
+ # PostgreSQLVacuum for details.
9
+ def vacuum(*args)
10
+ vacuumer = PostgreSQLVacuum.new(self, *args)
11
+ execute("#{vacuumer};")
12
+ end
13
+ end
14
+
15
+ # Creates queries for invoking VACUUM.
16
+ #
17
+ # This class is meant to be used by the PostgreSQLAdapter#vacuum method.
18
+ # VACUUMs can be performed against the database as a whole, on specific
19
+ # tables or on specific columns.
20
+ #
21
+ # ==== Examples
22
+ #
23
+ # ActiveRecord::Base.connection.vacuum
24
+ # # => VACUUM;
25
+ #
26
+ # ActiveRecord::Base.connection.vacuum(:full => true, :analyze => true)
27
+ # # => VACUUM FULL; # PostgreSQL < 9.0
28
+ # # => VACUUM (FULL); # PostgreSQL >= 9.0
29
+ #
30
+ # ActiveRecord::Base.connection.vacuum(:foos)
31
+ # # => VACUUM "foos";
32
+ #
33
+ # ActiveRecord::Base.connection.vacuum(:foos, :columns => [ :bar, :baz ])
34
+ # # => VACUUM (ANALYZE) "foos" ("bar", "baz");
35
+ #
36
+ # ==== Options
37
+ #
38
+ # * <tt>:full</tt>, <tt>:freeze</tt>, <tt>:verbose</tt> and
39
+ # <tt>:analyze</tt> are all supported.
40
+ # * <tt>:columns</tt> - specifies the columns to VACUUM. You must specify
41
+ # a table when using this option. This option also forces the :analyze
42
+ # option to true, as PostgreSQL doesn't like to try and VACUUM a column
43
+ # without analyzing it.
44
+ class PostgreSQLVacuum
45
+ VACUUM_OPTIONS = %w{
46
+ FULL FREEZE VERBOSE ANALYZE
47
+ }.freeze
48
+
49
+ attr_accessor :base, :table, :options
50
+
51
+ def initialize(base, *args)
52
+ if !args.length.between?(0, 2)
53
+ raise ArgumentError.new("Wrong number of arguments #{args.length} for 0-2")
54
+ end
55
+
56
+ options = args.extract_options!
57
+ table = args.first
58
+
59
+ if options[:columns]
60
+ if !table
61
+ raise ArgumentError.new("You must specify a table when using the :columns option.")
62
+ end
63
+
64
+ options[:analyze] = true
65
+ end
66
+
67
+ @base, @table, @options = base, table, options
68
+ end
69
+
70
+ def to_sql
71
+ vacuum_options = VACUUM_OPTIONS.select { |o|
72
+ o = o.downcase.to_sym
73
+ self.options[o.to_sym]
74
+ }
75
+
76
+ sql = 'VACUUM'
77
+
78
+ if !vacuum_options.empty?
79
+ sql << if ActiveRecord::PostgreSQLExtensions.SERVER_VERSION.to_f >= 9.0
80
+ " (#{vacuum_options.join(', ')})"
81
+ else
82
+ ' ' << VACUUM_OPTIONS.collect { |v|
83
+ v.upcase if vacuum_options.include?(v)
84
+ }.compact.join(' ')
85
+ end
86
+ end
87
+
88
+ sql << " #{base.quote_table_name(table)}" if self.table
89
+
90
+ if options[:columns]
91
+ sql << ' (' << Array(options[:columns]).collect { |column|
92
+ base.quote_column_name(column)
93
+ }.join(', ') << ')'
94
+ end
95
+
96
+ sql
97
+ end
98
+ alias :to_s :to_sql
99
+ end
100
+ end
101
+ end
@@ -1,7 +1,7 @@
1
1
 
2
2
  module ActiveRecord
3
3
  module PostgreSQLExtensions
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
6
6
  end
7
7
 
@@ -3,12 +3,23 @@ require 'active_record/connection_adapters/postgresql_adapter'
3
3
 
4
4
  module ActiveRecord
5
5
  module PostgreSQLExtensions
6
+ class << self
7
+ def SERVER_VERSION
8
+ return @SERVER_VERSION if defined?(@SERVER_VERSION)
9
+
10
+ @SERVER_VERSION = if (version_string = ::ActiveRecord::Base.connection.select_rows("SELECT pg_catalog.version()").flatten.first).present?
11
+ version_string =~ /^\s*PostgreSQL\s+([^\s]+)/
12
+ $1
13
+ end
14
+ end
15
+ end
6
16
  end
7
17
  end
8
18
 
9
19
  dirname = File.join(File.dirname(__FILE__), *%w{ active_record postgresql_extensions })
10
20
 
11
21
  %w{
22
+ features
12
23
  adapter_extensions
13
24
  constraints
14
25
  tables
@@ -28,6 +39,7 @@ dirname = File.join(File.dirname(__FILE__), *%w{ active_record postgresql_extens
28
39
  text_search
29
40
  extensions
30
41
  foreign_key_associations
42
+ vacuum
31
43
  }.each do |file|
32
44
  require File.join(dirname, file)
33
45
  end
@@ -2,7 +2,7 @@
2
2
  $: << File.dirname(__FILE__)
3
3
  require 'test_helper'
4
4
 
5
- class AdapterExtensionTests < Test::Unit::TestCase
5
+ class AdapterExtensionTests < MiniTest::Unit::TestCase
6
6
  include PostgreSQLExtensionsTestHelper
7
7
 
8
8
  def test_quote_table_name_with_schema_string
@@ -62,7 +62,7 @@ class AdapterExtensionTests < Test::Unit::TestCase
62
62
  %{SET SESSION ROLE "foo";}
63
63
  ], statements)
64
64
 
65
- assert_raise(ArgumentError) do
65
+ assert_raises(ArgumentError) do
66
66
  ARBC.set_role('foo', :duration => :nonsense)
67
67
  end
68
68
  end
@@ -141,4 +141,56 @@ class AdapterExtensionTests < Test::Unit::TestCase
141
141
  %{ALTER TABLE "foo" ENABLE TRIGGER "baz";}
142
142
  ], statements)
143
143
  end
144
+
145
+ def test_add_column_with_expression
146
+ Mig.add_column(:foo, :bar, :integer, :default => 100)
147
+ Mig.add_column(:foo, :bar, :integer, :default => {
148
+ :expression => '1 + 1'
149
+ })
150
+
151
+ Mig.add_column(:foo, :bar, :integer, :null => false, :default => {
152
+ :expression => '1 + 1'
153
+ })
154
+
155
+ if RUBY_PLATFORM == 'java' || ActiveRecord::VERSION::MAJOR <= 2
156
+ assert_equal([
157
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer},
158
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 100},
159
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer},
160
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 1 + 1;},
161
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer},
162
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 1 + 1;},
163
+ %{UPDATE "foo" SET "bar" = 1 + 1 WHERE "bar" IS NULL},
164
+ %{ALTER TABLE "foo" ALTER "bar" SET NOT NULL},
165
+ ], statements)
166
+ else
167
+ assert_equal([
168
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer DEFAULT 100},
169
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer DEFAULT 1 + 1},
170
+ %{ALTER TABLE "foo" ADD COLUMN "bar" integer DEFAULT 1 + 1 NOT NULL}
171
+ ], statements)
172
+ end
173
+ end
174
+
175
+ def test_change_column_with_expression
176
+ Mig.change_column(:foo, :bar, :integer, :default => 100)
177
+ Mig.change_column(:foo, :bar, :integer, :default => {
178
+ :expression => '1 + 1'
179
+ })
180
+
181
+ Mig.change_column(:foo, :bar, :integer, :null => false, :default => {
182
+ :expression => '1 + 1'
183
+ })
184
+
185
+ assert_equal([
186
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" TYPE integer},
187
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 100},
188
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" TYPE integer},
189
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 1 + 1;},
190
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" TYPE integer},
191
+ %{ALTER TABLE "foo" ALTER COLUMN "bar" SET DEFAULT 1 + 1;},
192
+ %{UPDATE "foo" SET "bar" = 1 + 1 WHERE "bar" IS NULL},
193
+ %{ALTER TABLE "foo" ALTER "bar" SET NOT NULL},
194
+ ], statements)
195
+ end
144
196
  end