activerecord-postgresql-extensions 0.1.0 → 0.2.0

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