activerecord-postgresql-extensions 0.0.7
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/MIT-LICENSE +23 -0
- data/README.rdoc +32 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/activerecord-postgresql-extensions.rb +30 -0
- data/lib/postgresql_extensions/foreign_key_associations.rb +367 -0
- data/lib/postgresql_extensions/postgresql_adapter_extensions.rb +646 -0
- data/lib/postgresql_extensions/postgresql_constraints.rb +579 -0
- data/lib/postgresql_extensions/postgresql_functions.rb +345 -0
- data/lib/postgresql_extensions/postgresql_geometry.rb +212 -0
- data/lib/postgresql_extensions/postgresql_indexes.rb +219 -0
- data/lib/postgresql_extensions/postgresql_languages.rb +80 -0
- data/lib/postgresql_extensions/postgresql_permissions.rb +322 -0
- data/lib/postgresql_extensions/postgresql_rules.rb +112 -0
- data/lib/postgresql_extensions/postgresql_schemas.rb +49 -0
- data/lib/postgresql_extensions/postgresql_sequences.rb +222 -0
- data/lib/postgresql_extensions/postgresql_tables.rb +308 -0
- data/lib/postgresql_extensions/postgresql_triggers.rb +131 -0
- data/lib/postgresql_extensions/postgresql_types.rb +17 -0
- data/lib/postgresql_extensions/postgresql_views.rb +103 -0
- data/postgresql-extensions.gemspec +50 -0
- data/test/adapter_test.rb +45 -0
- data/test/constraints_test.rb +98 -0
- data/test/functions_test.rb +112 -0
- data/test/geometry_test.rb +43 -0
- data/test/index_test.rb +68 -0
- data/test/languages_test.rb +48 -0
- data/test/permissions_test.rb +163 -0
- data/test/rules_test.rb +32 -0
- data/test/schemas_test.rb +43 -0
- data/test/sequences_test.rb +90 -0
- data/test/tables_test.rb +49 -0
- data/test/test_helper.rb +64 -0
- metadata +97 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class InvalidRuleEvent < ActiveRecordError #:nodoc:
|
6
|
+
def initialize(event)
|
7
|
+
super("Invalid rule event - #{event}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidRuleAction < ActiveRecordError #:nodoc:
|
12
|
+
def initialize(action)
|
13
|
+
super("Invalid rule action - #{action}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ConnectionAdapters
|
18
|
+
class PostgreSQLAdapter < AbstractAdapter
|
19
|
+
# Creates a PostgreSQL rule.
|
20
|
+
#
|
21
|
+
# +event+ can be one of <tt>:select</tt>, <tt>:insert</tt>,
|
22
|
+
# <tt>:update</tt> or <tt>:delete</tt>.
|
23
|
+
#
|
24
|
+
# +action+ can be one of <tt>:instead</tt> or <tt>:also</tt>.
|
25
|
+
#
|
26
|
+
# +commands+ is the actual query to rewrite to. commands can
|
27
|
+
# actually be "+NOTHING+", a String representing the commands
|
28
|
+
# or an Array of Strings if you have multiple commands you want to
|
29
|
+
# fire.
|
30
|
+
#
|
31
|
+
# ==== Options
|
32
|
+
#
|
33
|
+
# * <tt>:force</tt> - add an <tt>OR REPLACE</tt> clause to the
|
34
|
+
# command.
|
35
|
+
# * <tt>:conditions</tt> - a <tt>WHERE</tt> clause to limit the
|
36
|
+
# rule.
|
37
|
+
#
|
38
|
+
# ==== Examples
|
39
|
+
#
|
40
|
+
# ### ruby
|
41
|
+
# create_rule(
|
42
|
+
# 'check_it_out_rule',
|
43
|
+
# :select,
|
44
|
+
# :child,
|
45
|
+
# :instead,
|
46
|
+
# 'select * from public.another', :conditions => 'id = 1'
|
47
|
+
# )
|
48
|
+
# # => CREATE RULE "check_it_out_rule" AS ON SELECT TO "child" WHERE id = 1 DO INSTEAD select * from public.another;
|
49
|
+
def create_rule(name, event, table, action, commands, options = {})
|
50
|
+
execute PostgreSQLRuleDefinition.new(self, name, event, table, action, commands, options).to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
# Drops a PostgreSQL rule.
|
54
|
+
def drop_rule(name, table)
|
55
|
+
execute "DROP RULE #{quote_rule(name)} ON #{quote_table_name(table)}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates a PostgreSQL rule.
|
60
|
+
#
|
61
|
+
# The PostgreSQL rule system is basically a query-rewriter. You should
|
62
|
+
# take a look at the PostgreSQL documentation for more details, but the
|
63
|
+
# basic idea is that a rule can be set to fire on certain query events
|
64
|
+
# and will force the query to be rewritten before it is even sent to
|
65
|
+
# the query planner and executor.
|
66
|
+
#
|
67
|
+
# Generally speaking, you're probably going to want to stick to
|
68
|
+
# create_rule and drop_rule when working with rules.
|
69
|
+
class PostgreSQLRuleDefinition
|
70
|
+
attr_accessor :base, :name, :event, :table, :action, :commands, :options
|
71
|
+
|
72
|
+
def initialize(base, name, event, table, action, commands, options = {}) #:nodoc:
|
73
|
+
assert_valid_event(event)
|
74
|
+
assert_valid_action(action)
|
75
|
+
@base, @name, @event, @table, @action, @commands, @options =
|
76
|
+
base, name, event, table, action, commands, options
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_sql #:nodoc:
|
80
|
+
sql = 'CREATE '
|
81
|
+
sql << ' OR REPLACE ' if options[:force]
|
82
|
+
sql << "RULE #{base.quote_rule(name)} AS ON #{event.to_s.upcase} TO #{base.quote_table_name(table)} "
|
83
|
+
sql << "WHERE #{options[:conditions]} " if options[:conditions]
|
84
|
+
sql << "DO #{action.to_s.upcase} "
|
85
|
+
sql << if commands.to_s.upcase == 'NOTHING'
|
86
|
+
'NOTHING'
|
87
|
+
elsif commands.is_a?(Array)
|
88
|
+
'(' << commands.collect(&:to_s).join(';') << ')'
|
89
|
+
else
|
90
|
+
commands.to_s
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias :to_s :to_sql
|
94
|
+
|
95
|
+
private
|
96
|
+
EVENTS = [ 'select', 'insert', 'update', 'delete' ].freeze
|
97
|
+
ACTIONS = [ 'instead', 'also' ].freeze
|
98
|
+
|
99
|
+
def assert_valid_event(event) #:nodoc:
|
100
|
+
if !EVENTS.include? event.to_s
|
101
|
+
raise ActiveRecord::InvalidRuleEvent.new(event)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def assert_valid_action(action) #:nodoc:
|
106
|
+
if !ACTIONS.include? action.to_s
|
107
|
+
raise ActiveRecord::InvalidRuleAction.new(action)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
class PostgreSQLAdapter < AbstractAdapter
|
7
|
+
# Creates a new PostgreSQL schema.
|
8
|
+
#
|
9
|
+
# Note that you can grant privileges on schemas using the
|
10
|
+
# grant_schema_privileges method and revoke them using
|
11
|
+
# revoke_schema_privileges.
|
12
|
+
#
|
13
|
+
# ==== Options
|
14
|
+
#
|
15
|
+
# * <tt>:authorization</tt> - adds an AUTHORIZATION clause. This is
|
16
|
+
# used to set the owner of the schema. This can be changed with
|
17
|
+
# alter_schema_owner as necessary.
|
18
|
+
def create_schema schema, options = {}
|
19
|
+
sql = "CREATE SCHEMA #{quote_schema(schema)}"
|
20
|
+
sql << " AUTHORIZATION #{quote_role(options[:authorization])}" if options[:authorization]
|
21
|
+
execute sql
|
22
|
+
end
|
23
|
+
|
24
|
+
# Drops a schema.
|
25
|
+
#
|
26
|
+
# ==== Options
|
27
|
+
#
|
28
|
+
# * <tt>:if_exists</tt> - adds IF EXISTS.
|
29
|
+
# * <tt>:cascade</tt> - adds CASCADE.
|
30
|
+
def drop_schema schemas, options = {}
|
31
|
+
sql = 'DROP SCHEMA '
|
32
|
+
sql << 'IF EXISTS ' if options[:if_exists]
|
33
|
+
sql << Array(schemas).collect { |s| quote_schema(s) }.join(', ')
|
34
|
+
sql << ' CASCADE' if options[:cascade]
|
35
|
+
execute sql
|
36
|
+
end
|
37
|
+
|
38
|
+
# Alter's a schema's name.
|
39
|
+
def alter_schema_name old_schema, new_schema
|
40
|
+
execute "ALTER SCHEMA #{quote_schema(old_schema)} RENAME TO #{quote_schema(new_schema)}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Changes a schema's owner.
|
44
|
+
def alter_schema_owner schema, role
|
45
|
+
execute "ALTER SCHEMA #{quote_schema(schema)} OWNER TO #{quote_schema(role)}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class InvalidSequenceAction < ActiveRecordError #:nodoc:
|
6
|
+
def initialize(action)
|
7
|
+
super("Invalid sequence action - #{action}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidSequenceOptions < ActiveRecordError #:nodoc:
|
12
|
+
end
|
13
|
+
|
14
|
+
module ConnectionAdapters
|
15
|
+
class PostgreSQLAdapter < AbstractAdapter
|
16
|
+
# Creates a sequence.
|
17
|
+
#
|
18
|
+
# Note that you can grant privileges on sequences using the
|
19
|
+
# grant_sequence_privileges method and revoke them using
|
20
|
+
# revoke_sequence_privileges.
|
21
|
+
#
|
22
|
+
# ==== Options
|
23
|
+
#
|
24
|
+
# * <tt>:temporary</tt> - creates a temporary sequence.
|
25
|
+
# * <tt>:incement</tt> - sets the sequence increment value.
|
26
|
+
# * <tt>:min_value</tt> - sets a minimum value for the sequence.
|
27
|
+
# If this value is <tt>nil</tt> or <tt>false</tt>, we'll go with
|
28
|
+
# "NO MINVALUE".
|
29
|
+
# * <tt>:max_value</tt> - same as <tt>:min_value</tt> but for
|
30
|
+
# maximum values. Mindblowing.
|
31
|
+
# * <tt>:start</tt> - the initial value of the sequence.
|
32
|
+
# * <tt>:cache</tt> - the number of future values to cache in
|
33
|
+
# the sequence. This is generally dangerous to mess with, so be
|
34
|
+
# sure to refer to the PostgreSQL documentation for reasons why.
|
35
|
+
# * <tt>:cycle</tt> - whether or not the sequence should cycle.
|
36
|
+
# * <tt>:owned_by</tt> - this refers to the table and column that
|
37
|
+
# a sequence is owned by. If that column/table were to be
|
38
|
+
# dropped in the future, for instance, the sequence would be
|
39
|
+
# automatically dropped with it. This option can be set using
|
40
|
+
# an Array (as in <tt>[ table, column ]</tt>) or a Hash
|
41
|
+
# (as in <tt>{ :table => 'foo', :column => 'bar' }</tt>).
|
42
|
+
#
|
43
|
+
# ==== Example
|
44
|
+
#
|
45
|
+
# ### ruby
|
46
|
+
# create_sequence(
|
47
|
+
# 'what_a_sequence_of_events',
|
48
|
+
# :increment => 2,
|
49
|
+
# :cache => 2,
|
50
|
+
# :min_value => nil,
|
51
|
+
# :max_value => 10,
|
52
|
+
# :owned_by => [ :foo, :id ]
|
53
|
+
# )
|
54
|
+
# # => CREATE SEQUENCE "what_a_sequence_of_events" INCREMENT BY 2
|
55
|
+
# # NO MINVALUE MAXVALUE 10 CACHE 2 OWNED BY "foo"."id";
|
56
|
+
def create_sequence(name, options = {})
|
57
|
+
execute PostgreSQLSequenceDefinition.new(self, :create, name, options).to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
# Drops a sequence.
|
61
|
+
#
|
62
|
+
# ==== Options
|
63
|
+
#
|
64
|
+
# * <tt>:if_exists</tt> - adds IF EXISTS.
|
65
|
+
# * <tt>:cascade</tt> - cascades the operation down to objects
|
66
|
+
# referring to the sequence.
|
67
|
+
def drop_sequence(name, options = {})
|
68
|
+
sql = 'DROP SEQUENCE '
|
69
|
+
sql << 'IF EXISTS ' if options[:if_exists]
|
70
|
+
sql << Array(name).collect { |s| quote_sequence(s) }.join(', ')
|
71
|
+
sql << ' CASCADE' if options[:cascade]
|
72
|
+
execute sql
|
73
|
+
end
|
74
|
+
|
75
|
+
# Renames the sequence.
|
76
|
+
def rename_sequence(name, rename, options = {})
|
77
|
+
execute "ALTER SEQUENCE #{quote_sequence(name)} RENAME TO #{quote_generic_ignore_schema(rename)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Alters the sequence's schema.
|
81
|
+
def alter_sequence_schema(name, schema, options = {})
|
82
|
+
execute "ALTER SEQUENCE #{quote_sequence(name)} SET SCHEMA #{quote_schema(schema)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Alters any of the various options for a sequence. See
|
86
|
+
# create_sequence for details on the available options. In addition
|
87
|
+
# to the options provided by create_sequence, there is also the
|
88
|
+
# <tt>:restart_with</tt> option, which resets the sequence to
|
89
|
+
# a new starting value and sets the <tt>is_called</tt> flag to
|
90
|
+
# false, which would be the equivalent of calling the PostgreSQL
|
91
|
+
# function <tt>setval</tt> with a false value in the third
|
92
|
+
# parameter.
|
93
|
+
def alter_sequence(name, options = {})
|
94
|
+
execute PostgreSQLSequenceDefinition.new(self, :alter, name, options).to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
# Calls the <tt>setval</tt> function on the sequence.
|
98
|
+
#
|
99
|
+
# ==== Options
|
100
|
+
#
|
101
|
+
# * <tt>:is_called</tt> - the value to set in the third argument
|
102
|
+
# to the function call, which is, appropriately enough, the
|
103
|
+
# <tt>is_called</tt> argument. The default value is true.
|
104
|
+
def set_sequence_value(name, value, options = {})
|
105
|
+
options = {
|
106
|
+
:is_called => true
|
107
|
+
}.merge(options)
|
108
|
+
|
109
|
+
execute "SELECT setval(#{quote(name)}, #{value.to_i}, " <<
|
110
|
+
if options[:is_called]
|
111
|
+
'true'
|
112
|
+
else
|
113
|
+
'false'
|
114
|
+
end <<
|
115
|
+
')'
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns an Array of available sequences.
|
119
|
+
def sequences(name = nil)
|
120
|
+
query(<<-SQL, name).map { |row| row[0] }
|
121
|
+
SELECT c.relname AS sequencename
|
122
|
+
FROM pg_class c
|
123
|
+
WHERE c.relkind = 'S'::"char";
|
124
|
+
SQL
|
125
|
+
end
|
126
|
+
|
127
|
+
def sequence_exists?(name)
|
128
|
+
sequences.include?(name.to_s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Class used to create or alter sequences. Generally you should be
|
133
|
+
# using PostgreSQLAdapter#create_sequence and its various sequence
|
134
|
+
# manipulation functions rather than using this class directly.
|
135
|
+
class PostgreSQLSequenceDefinition
|
136
|
+
attr_accessor :base, :action, :name, :options
|
137
|
+
|
138
|
+
def initialize(base, action, name, options = {}) #:nodoc:
|
139
|
+
assert_valid_owned_by(options)
|
140
|
+
assert_valid_action(action)
|
141
|
+
|
142
|
+
@base, @action, @name, @options = base, action, name, options
|
143
|
+
end
|
144
|
+
|
145
|
+
def to_sql #:nodoc:
|
146
|
+
sql = Array.new
|
147
|
+
if action == :create
|
148
|
+
sql << 'CREATE'
|
149
|
+
sql << 'TEMPORARY' if options[:temporary]
|
150
|
+
else
|
151
|
+
sql << 'ALTER'
|
152
|
+
end
|
153
|
+
sql << "SEQUENCE #{base.quote_sequence(name)}"
|
154
|
+
sql << "INCREMENT BY #{options[:increment].to_i}" if options[:increment]
|
155
|
+
if options.has_key?(:min_value)
|
156
|
+
sql << case options[:min_value]
|
157
|
+
when NilClass, FalseClass
|
158
|
+
'NO MINVALUE'
|
159
|
+
else
|
160
|
+
"MINVALUE #{options[:min_value].to_i}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if options.has_key?(:max_value)
|
165
|
+
sql << case options[:max_value]
|
166
|
+
when NilClass, FalseClass
|
167
|
+
'NO MAXVALUE'
|
168
|
+
else
|
169
|
+
"MAXVALUE #{options[:max_value].to_i}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
sql << "START WITH #{options[:start].to_i}" if options[:start]
|
173
|
+
sql << "CACHE #{options[:cache].to_i}" if options[:cache]
|
174
|
+
|
175
|
+
if options.has_key?(:cycle)
|
176
|
+
sql << (options[:cycle] ? 'CYCLE' : 'NO CYCLE')
|
177
|
+
end
|
178
|
+
|
179
|
+
if options.has_key?(:owned_by)
|
180
|
+
table_column = if options[:owned_by].is_a?(Hash)
|
181
|
+
[ options[:owned_by][:table], options[:owned_by][:column] ]
|
182
|
+
elsif options[:owned_by].is_a?(Array)
|
183
|
+
options[:owned_by]
|
184
|
+
end
|
185
|
+
|
186
|
+
sql << 'OWNED BY ' + if options[:owned_by] == :none
|
187
|
+
'NONE'
|
188
|
+
else
|
189
|
+
"#{base.quote_table_name(table_column.first)}.#{base.quote_column_name(table_column.last)}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if action != :create && options.has_key?(:restart_with)
|
194
|
+
sql << "RESTART WITH #{options[:restart_with].to_i}"
|
195
|
+
end
|
196
|
+
sql.join(' ')
|
197
|
+
end
|
198
|
+
alias :to_s :to_sql
|
199
|
+
|
200
|
+
private
|
201
|
+
def assert_valid_owned_by(options) #:nodoc:
|
202
|
+
if options.has_key?(:owned_by)
|
203
|
+
begin
|
204
|
+
if options[:owned_by].is_a?(Hash)
|
205
|
+
raise if !(options[:owned_by].keys.sort == [ :column, :table ])
|
206
|
+
elsif options[:owned_by].is_a?(Array)
|
207
|
+
raise if options[:owned_by].length != 2
|
208
|
+
end
|
209
|
+
rescue
|
210
|
+
raise ActiveRecord::InvalidSequenceOptions.new("Invalid :owned_by options")
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def assert_valid_action(action) #:nodoc:
|
216
|
+
if ![ :create, :alter ].include?(action)
|
217
|
+
raise ActiveRecord::InvalidSequenceAction.new(action)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,308 @@
|
|
1
|
+
|
2
|
+
module ActiveRecord
|
3
|
+
class InvalidLikeTypes < ActiveRecordError #:nodoc:
|
4
|
+
def initialize(likes)
|
5
|
+
super("Invalid LIKE INCLUDING/EXCLUDING types - #{likes.inspect}")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidTableOptions < ActiveRecordError #:nodoc:
|
10
|
+
end
|
11
|
+
|
12
|
+
module ConnectionAdapters
|
13
|
+
class PostgreSQLAdapter < AbstractAdapter
|
14
|
+
# Set the schema of a table.
|
15
|
+
def alter_table_schema table_name, schema, options = {}
|
16
|
+
execute "ALTER TABLE #{quote_schema(table_name)} SET SCHEMA #{quote_schema(schema)}"
|
17
|
+
end
|
18
|
+
|
19
|
+
alias :original_create_table :create_table
|
20
|
+
# Creates a new table. We've expanded the capabilities of the
|
21
|
+
# standard ActiveRecord create_table method to included a host of
|
22
|
+
# PostgreSQL-specific functionality.
|
23
|
+
#
|
24
|
+
# === PostgreSQL-specific Do-dads
|
25
|
+
#
|
26
|
+
# PostgreSQL allows for a couple of nifty table creation options
|
27
|
+
# that ActiveRecord usually doesn't account for, so we're filling
|
28
|
+
# in the blanks here.
|
29
|
+
#
|
30
|
+
# * <tt>:inherits</tt> - PostgreSQL allows you to create tables
|
31
|
+
# that inherit the properties of another. PostgreSQL is
|
32
|
+
# sometimes referred to as an Object-Relational DBMS rather
|
33
|
+
# than a straight-up RDBMS because of stuff like this.
|
34
|
+
# * <tt>:on_commit</tt> - allows you to define the behaviour of
|
35
|
+
# temporary tables. Allowed values are <tt>:preserve_rows</tt>
|
36
|
+
# (the default, which causes the temporary table to retain its
|
37
|
+
# rows at the end of a transaction), <tt>:delete_rows</tt>
|
38
|
+
# (which truncates the table at the end of a transaction) and
|
39
|
+
# <tt>:drop</tt> (which drops the table at the end of a
|
40
|
+
# transaction).
|
41
|
+
# * <tt>:tablespace</tt> - allows you to set the tablespace of a
|
42
|
+
# table.
|
43
|
+
# * <tt>:force</tt> - force a table to be dropped before trying to
|
44
|
+
# create it. This will pass <tt>:if_exists => true</tt> to the
|
45
|
+
# drop_table method.
|
46
|
+
# * <tt>:cascade_drop</tt> - when using the <tt>:force</tt>, this
|
47
|
+
# Jedi mindtrick will pass along the :cascade option to
|
48
|
+
# drop_table.
|
49
|
+
#
|
50
|
+
# We're expanding the doors of table definition perception with
|
51
|
+
# this exciting new addition to the world of ActiveRecord
|
52
|
+
# PostgreSQL adapters.
|
53
|
+
#
|
54
|
+
# create_table generally behaves like the standard ActiveRecord
|
55
|
+
# create_table method with a couple of notable exceptions:
|
56
|
+
#
|
57
|
+
# * you can add column constraints.
|
58
|
+
# * you can add constraints to the table itself.
|
59
|
+
# * you can add LIKE and INHERITS clauses to the definition.
|
60
|
+
#
|
61
|
+
# See the PostgreSQL documentation for more detailed on these
|
62
|
+
# sorts of things. Odds are that you'll probably recognize what
|
63
|
+
# we're referring to here if you're bothering to use this
|
64
|
+
# plugin, eh?
|
65
|
+
#
|
66
|
+
# Also, do note that you can grant privileges on tables using the
|
67
|
+
# grant_table_privileges method and revoke them using
|
68
|
+
# revoke_table_privileges.
|
69
|
+
#
|
70
|
+
# ==== Examples
|
71
|
+
#
|
72
|
+
# ### ruby
|
73
|
+
# create_table(:foo, :inherits => :parent) do |t|
|
74
|
+
# t.integer :bar_id, :references => :bar
|
75
|
+
# t.like :base, :including => [ :defaults, :indexes ], :excluding => :constraints
|
76
|
+
# t.check_constraint "bar_id < 100"
|
77
|
+
# t.unique_constraint :bar_id
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# # Produces:
|
81
|
+
# #
|
82
|
+
# # CREATE TABLE "foo" (
|
83
|
+
# # "id" serial primary key,
|
84
|
+
# # "bar_id" integer DEFAULT NULL NULL,
|
85
|
+
# # LIKE "base" INCLUDING DEFAULTS INCLUDING INDEXES EXCLUDING CONSTRAINTS,
|
86
|
+
# # FOREIGN KEY ("bar_id") REFERENCES "bar",
|
87
|
+
# # CHECK (bar_id < 100),
|
88
|
+
# # UNIQUE ("bar_id")
|
89
|
+
# # ) INHERITS ("parent");
|
90
|
+
#
|
91
|
+
# This is a fairly convoluted example, but there you have it.
|
92
|
+
#
|
93
|
+
# Beyond these differences, create_table acts like the original
|
94
|
+
# ActiveRecord create_table, which you can actually still access
|
95
|
+
# using the original_create_table method if you really, really want
|
96
|
+
# to.
|
97
|
+
#
|
98
|
+
# Be sure to refer to the PostgreSQL documentation for details on
|
99
|
+
# data definition and such.
|
100
|
+
def create_table(table_name, options = {})
|
101
|
+
if options[:force]
|
102
|
+
drop_table(table_name, { :if_exists => true, :cascade => options[:cascade_drop] })
|
103
|
+
end
|
104
|
+
|
105
|
+
table_definition = PostgreSQLTableDefinition.new(self, table_name, options)
|
106
|
+
yield table_definition
|
107
|
+
|
108
|
+
execute table_definition.to_s
|
109
|
+
unless table_definition.post_processing.blank?
|
110
|
+
table_definition.post_processing.each do |pp|
|
111
|
+
execute pp
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
alias :original_drop_table :drop_table
|
117
|
+
# Drops a table. This method is expanded beyond the standard
|
118
|
+
# ActiveRecord drop_table method to allow for a couple of
|
119
|
+
# PostgreSQL-specific options:
|
120
|
+
#
|
121
|
+
# * <tt>:if_exists</tt> - adds an IF EXISTS clause to the query.
|
122
|
+
# In absence of this option, an exception will be raised if you
|
123
|
+
# try to drop a table that doesn't exist.
|
124
|
+
# * <tt>:cascade</tt> - adds a CASCADE clause to the query. This
|
125
|
+
# will cause references to this table like foreign keys to be
|
126
|
+
# dropped as well. See the PostgreSQL documentation for details.
|
127
|
+
#
|
128
|
+
# You can still access the original method via original_drop_table.
|
129
|
+
def drop_table(tables, options = {})
|
130
|
+
sql = 'DROP TABLE '
|
131
|
+
sql << 'IF EXISTS ' if options[:if_exists]
|
132
|
+
sql << Array(tables).collect { |t| quote_table_name(t) }.join(', ')
|
133
|
+
sql << ' CASCADE' if options[:cascade]
|
134
|
+
execute sql
|
135
|
+
end
|
136
|
+
|
137
|
+
alias :original_rename_table :rename_table
|
138
|
+
# Renames a table. We're overriding the original rename_table so
|
139
|
+
# that we can take advantage of our super schema quoting
|
140
|
+
# capabilities. You can still access the original method via
|
141
|
+
# original_rename_table.
|
142
|
+
def rename_table(name, new_name, options = {})
|
143
|
+
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_generic_ignore_schema(new_name)}"
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
ON_COMMIT_VALUES = [ 'preserve_rows', 'delete_rows', 'drop' ].freeze
|
148
|
+
|
149
|
+
def assert_valid_on_commit(temp, on_commit)
|
150
|
+
unless on_commit.nil?
|
151
|
+
if !ON_COMMIT_VALUES.include?(on_commit.to_s.downcase)
|
152
|
+
raise ActiveRecord::InvalidTableOptions.new("Invalid ON COMMIT value - #{on_commit}")
|
153
|
+
elsif !temp
|
154
|
+
raise ActiveRecord::InvalidTableOptions.new("ON COMMIT can only be used with temporary tables")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Creates a PostgreSQL table definition. This class isn't really meant
|
161
|
+
# to be used directly. Instead, see PostgreSQLAdapter#create_table
|
162
|
+
# for usage.
|
163
|
+
#
|
164
|
+
# Beyond our various PostgreSQL-specific extensions, we've also added
|
165
|
+
# the <tt>post_processing</tt> member, which allows you to tack on
|
166
|
+
# some SQL statements to run after creating the table. This member
|
167
|
+
# should be an Array of SQL statements to run once the table has
|
168
|
+
# been created. See the source code for PostgreSQLAdapter#create_table
|
169
|
+
# and PostgreSQLTableDefinition#geometry for an example of its use.
|
170
|
+
class PostgreSQLTableDefinition < TableDefinition
|
171
|
+
attr_accessor :base, :table_name, :options, :post_processing
|
172
|
+
|
173
|
+
def initialize(base, table_name, options = {}) #:nodoc:
|
174
|
+
@table_constraints = Array.new
|
175
|
+
@table_name, @options = table_name, options
|
176
|
+
super(base)
|
177
|
+
|
178
|
+
self.primary_key(
|
179
|
+
options[:primary_key] || Base.get_primary_key(table_name)
|
180
|
+
) unless options[:id] == false
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_sql #:nodoc:
|
184
|
+
sql = 'CREATE '
|
185
|
+
sql << 'TEMPORARY ' if options[:temporary]
|
186
|
+
sql << "TABLE #{base.quote_table_name(table_name)} (\n "
|
187
|
+
|
188
|
+
ary = @columns.collect(&:to_sql)
|
189
|
+
ary << @like if @like
|
190
|
+
ary << @table_constraints unless @table_constraints.empty?
|
191
|
+
sql << ary * ",\n "
|
192
|
+
sql << "\n)"
|
193
|
+
|
194
|
+
sql << "INHERITS (" << Array(options[:inherits]).collect { |i| base.quote_table_name(i) }.join(', ') << ')' if options[:inherits]
|
195
|
+
sql << "ON COMMIT #{options[:on_commit].to_s.upcase}" if options[:on_commit]
|
196
|
+
sql << "#{options[:options]}" if options[:options]
|
197
|
+
sql << "TABLESPACE #{base.quote_tablespace(options[:tablespace])}" if options[:tablespace]
|
198
|
+
sql
|
199
|
+
end
|
200
|
+
alias :to_s :to_sql
|
201
|
+
|
202
|
+
# Creates a LIKE statement for use in a table definition.
|
203
|
+
#
|
204
|
+
# ==== Options
|
205
|
+
#
|
206
|
+
# * <tt>:including</tt> and <tt>:excluding</tt> - set options for
|
207
|
+
# the INCLUDING and EXCLUDING clauses in a LIKE statement. Valid
|
208
|
+
# values are <tt>:constraints</tt>, <tt>:defaults</tt> and
|
209
|
+
# <tt>:indexes</tt>. You can set one or more by using an Array.
|
210
|
+
#
|
211
|
+
# See the PostgreSQL documentation for details on how to use
|
212
|
+
# LIKE. Be sure to take note as to how it differs from INHERITS.
|
213
|
+
#
|
214
|
+
# Also, be sure to note that, like, this LIKE isn't, like, the
|
215
|
+
# LIKE you use in a WHERE condition. This is, PostgreSQL's
|
216
|
+
# own special LIKE clause for table definitions. Like.
|
217
|
+
def like(parent_table, options = {})
|
218
|
+
assert_valid_like_types(options[:includes])
|
219
|
+
assert_valid_like_types(options[:excludes])
|
220
|
+
|
221
|
+
# Huh? Whyfor I dun this?
|
222
|
+
# @like = base.with_schema(@schema) { "LIKE #{base.quote_table_name(parent_table)}" }
|
223
|
+
@like = "LIKE #{@base.quote_table_name(parent_table)}"
|
224
|
+
|
225
|
+
if options[:including]
|
226
|
+
@like << Array(options[:including]).collect { |l| " INCLUDING #{l.to_s.upcase}" }.join
|
227
|
+
end
|
228
|
+
|
229
|
+
if options[:excluding]
|
230
|
+
@like << Array(options[:excluding]).collect { |l| " EXCLUDING #{l.to_s.upcase}" }.join
|
231
|
+
end
|
232
|
+
@like
|
233
|
+
end
|
234
|
+
|
235
|
+
# Add a CHECK constraint to the table. See
|
236
|
+
# PostgreSQLCheckConstraint for more details.
|
237
|
+
def check_constraint(expression, options = {})
|
238
|
+
@table_constraints << PostgreSQLCheckConstraint.new(@base, expression, options)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Add a UNIQUE constraint to the table. See
|
242
|
+
# PostgreSQLUniqueConstraint for more details.
|
243
|
+
def unique_constraint(columns, options = {})
|
244
|
+
@table_constraints << PostgreSQLUniqueConstraint.new(@base, columns, options)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Add a FOREIGN KEY constraint to the table. See
|
248
|
+
# PostgreSQLForeignKeyConstraint for more details.
|
249
|
+
def foreign_key(columns, ref_table, *args)
|
250
|
+
@table_constraints << PostgreSQLForeignKeyConstraint.new(@base, columns, ref_table, *args)
|
251
|
+
end
|
252
|
+
|
253
|
+
def column_with_constraints(name, type, *args) #:nodoc:
|
254
|
+
options = args.extract_options!
|
255
|
+
check = options.delete(:check)
|
256
|
+
references = options.delete(:references)
|
257
|
+
unique = options.delete(:unique)
|
258
|
+
column_without_constraints(name, type, options)
|
259
|
+
|
260
|
+
if check
|
261
|
+
@table_constraints << Array(check).collect do |c|
|
262
|
+
if c.is_a?(Hash)
|
263
|
+
PostgreSQLCheckConstraint.new(@base, c.delete(:expression), c)
|
264
|
+
else
|
265
|
+
PostgreSQLCheckConstraint.new(@base, c)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
if references
|
271
|
+
ref_table, ref_options = if references.is_a?(Hash)
|
272
|
+
[ references.delete(:table), references ]
|
273
|
+
else
|
274
|
+
[ references, {} ]
|
275
|
+
end
|
276
|
+
|
277
|
+
@table_constraints << PostgreSQLForeignKeyConstraint.new(
|
278
|
+
@base,
|
279
|
+
name,
|
280
|
+
ref_table,
|
281
|
+
ref_options
|
282
|
+
)
|
283
|
+
end
|
284
|
+
|
285
|
+
if unique
|
286
|
+
unless unique.is_a?(Hash)
|
287
|
+
unique = {}
|
288
|
+
end
|
289
|
+
@table_constraints << PostgreSQLUniqueConstraint.new(@base, name, unique)
|
290
|
+
end
|
291
|
+
self
|
292
|
+
end
|
293
|
+
alias_method_chain :column, :constraints
|
294
|
+
|
295
|
+
private
|
296
|
+
LIKE_TYPES = [ 'defaults', 'constraints', 'indexes' ].freeze
|
297
|
+
|
298
|
+
def assert_valid_like_types(likes) #:nodoc:
|
299
|
+
unless likes.blank?
|
300
|
+
check_likes = Array(likes).collect(&:to_s) - LIKE_TYPES
|
301
|
+
if !check_likes.empty?
|
302
|
+
raise ActiveRecord::InvalidLikeTypes.new(check_likes)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|