schema_plus 0.4.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -1
- data/.travis.yml +6 -9
- data/Gemfile +0 -4
- data/README.rdoc +168 -70
- data/Rakefile +58 -47
- data/gemfiles/rails-3.2/Gemfile.base +4 -0
- data/gemfiles/rails-3.2/Gemfile.mysql +4 -0
- data/gemfiles/rails-3.2/Gemfile.mysql2 +4 -0
- data/gemfiles/rails-3.2/Gemfile.postgresql +4 -0
- data/gemfiles/rails-3.2/Gemfile.sqlite3 +4 -0
- data/lib/schema_plus.rb +2 -0
- data/lib/schema_plus/active_record/column_options_handler.rb +73 -32
- data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +60 -31
- data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +7 -2
- data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +2 -1
- data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +19 -1
- data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +68 -17
- data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +28 -3
- data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +27 -1
- data/lib/schema_plus/active_record/db_default.rb +19 -0
- data/lib/schema_plus/active_record/foreign_keys.rb +40 -32
- data/lib/schema_plus/active_record/schema_dumper.rb +7 -3
- data/lib/schema_plus/version.rb +1 -1
- data/runspecs +5 -8
- data/schema_plus.gemspec +2 -5
- data/spec/column_definition_spec.rb +18 -1
- data/spec/column_spec.rb +39 -2
- data/spec/connection_spec.rb +1 -1
- data/spec/connections/mysql/connection.rb +1 -1
- data/spec/connections/mysql2/connection.rb +1 -1
- data/spec/connections/postgresql/connection.rb +1 -1
- data/spec/foreign_key_definition_spec.rb +0 -4
- data/spec/foreign_key_spec.rb +37 -13
- data/spec/index_definition_spec.rb +54 -2
- data/spec/index_spec.rb +59 -15
- data/spec/migration_spec.rb +336 -85
- data/spec/multiple_schemas_spec.rb +127 -0
- data/spec/schema_dumper_spec.rb +65 -25
- data/spec/schema_spec.rb +16 -18
- data/spec/spec_helper.rb +19 -18
- data/spec/support/matchers/reference.rb +7 -1
- data/spec/views_spec.rb +5 -2
- metadata +43 -54
- data/gemfiles/Gemfile.rails-2.3 +0 -6
- data/gemfiles/Gemfile.rails-2.3.lock +0 -65
- data/gemfiles/Gemfile.rails-3.0 +0 -5
- data/gemfiles/Gemfile.rails-3.0.lock +0 -113
- data/gemfiles/Gemfile.rails-3.1 +0 -5
- data/gemfiles/Gemfile.rails-3.1.lock +0 -123
- data/gemfiles/Gemfile.rails-3.2 +0 -5
- data/gemfiles/Gemfile.rails-3.2.lock +0 -121
- data/spec/models/comment.rb +0 -2
- data/spec/models/post.rb +0 -2
- data/spec/models/user.rb +0 -2
- data/spec/rails3_migration_spec.rb +0 -144
data/lib/schema_plus.rb
CHANGED
@@ -3,6 +3,7 @@ require 'valuable'
|
|
3
3
|
require 'schema_plus/version'
|
4
4
|
require 'schema_plus/active_record/base'
|
5
5
|
require 'schema_plus/active_record/column_options_handler'
|
6
|
+
require 'schema_plus/active_record/db_default'
|
6
7
|
require 'schema_plus/active_record/foreign_keys'
|
7
8
|
require 'schema_plus/active_record/connection_adapters/table_definition'
|
8
9
|
require 'schema_plus/active_record/connection_adapters/schema_statements'
|
@@ -132,6 +133,7 @@ module SchemaPlus
|
|
132
133
|
::ActiveRecord::Base.send(:include, SchemaPlus::ActiveRecord::Base)
|
133
134
|
::ActiveRecord::Schema.send(:include, SchemaPlus::ActiveRecord::Schema)
|
134
135
|
::ActiveRecord::SchemaDumper.send(:include, SchemaPlus::ActiveRecord::SchemaDumper)
|
136
|
+
::ActiveRecord.const_set(:DB_DEFAULT, SchemaPlus::ActiveRecord::DB_DEFAULT)
|
135
137
|
end
|
136
138
|
|
137
139
|
end
|
@@ -2,48 +2,80 @@ module SchemaPlus::ActiveRecord
|
|
2
2
|
module ColumnOptionsHandler
|
3
3
|
def schema_plus_handle_column_options(table_name, column_name, column_options, opts = {}) #:nodoc:
|
4
4
|
config = opts[:config] || SchemaPlus.config
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
fk_args = get_fk_args(table_name, column_name, column_options, config)
|
6
|
+
|
7
|
+
# remove existing fk and auto-generated index in case of change to existing column
|
8
|
+
if fk_args # includes :none for explicitly off
|
9
|
+
remove_foreign_key_if_exists(table_name, column_name)
|
10
|
+
remove_auto_index_if_exists(table_name, column_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
fk_args = nil if fk_args == :none
|
14
|
+
|
15
|
+
# create index if requested explicity or implicitly due to auto_index
|
16
|
+
index = column_options[:index]
|
17
|
+
if index.nil? and fk_args && config.foreign_keys.auto_index?
|
18
|
+
index = { :name => auto_index_name(table_name, column_name) }
|
19
|
+
end
|
20
|
+
column_index(table_name, column_name, index) if index
|
21
|
+
|
22
|
+
if fk_args
|
23
|
+
references = fk_args.delete(:references)
|
24
|
+
add_foreign_key(table_name, column_name, references.first, references.last, fk_args)
|
14
25
|
end
|
15
26
|
end
|
16
27
|
|
17
28
|
protected
|
18
29
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
#
|
31
|
-
def get_references(table_name, column_name, column_options = {}, config = {}) #:nodoc:
|
30
|
+
def get_fk_args(table_name, column_name, column_options = {}, config = {}) #:nodoc:
|
31
|
+
|
32
|
+
args = nil
|
33
|
+
|
34
|
+
if column_options.has_key?(:foreign_key)
|
35
|
+
args = column_options[:foreign_key]
|
36
|
+
return :none unless args
|
37
|
+
args = {} if args == true
|
38
|
+
return :none if args.has_key?(:references) and not args[:references]
|
39
|
+
end
|
40
|
+
|
32
41
|
if column_options.has_key?(:references)
|
33
42
|
references = column_options[:references]
|
34
|
-
|
35
|
-
references
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
return :none unless references
|
44
|
+
args = (args || {}).reverse_merge(:references => references)
|
45
|
+
end
|
46
|
+
|
47
|
+
args ||= {} if config.foreign_keys.auto_create? and column_name =~ /_id$/
|
48
|
+
|
49
|
+
return nil if args.nil?
|
50
|
+
|
51
|
+
args[:references] ||= case column_name.to_s
|
52
|
+
when 'parent_id'
|
53
|
+
[table_name, :id]
|
54
|
+
when /^(.*)_id$/
|
55
|
+
references_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
|
56
|
+
[references_table_name, :id]
|
57
|
+
else
|
58
|
+
references_table_name = ActiveRecord::Base.pluralize_table_names ? column_name.to_s.pluralize : column_name
|
59
|
+
end
|
60
|
+
args[:references] = [args[:references], :id] unless args[:references].is_a? Array
|
61
|
+
|
62
|
+
[:on_update, :on_delete, :deferrable].each do |shortcut|
|
63
|
+
args[shortcut] ||= column_options[shortcut] if column_options.has_key? shortcut
|
44
64
|
end
|
65
|
+
|
66
|
+
args[:on_update] ||= config.foreign_keys.on_update
|
67
|
+
args[:on_delete] ||= config.foreign_keys.on_delete
|
68
|
+
|
69
|
+
args
|
70
|
+
end
|
71
|
+
|
72
|
+
def remove_foreign_key_if_exists(table_name, column_name) #:nodoc:
|
73
|
+
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name.to_s) rescue [] # no fks if table_name doesn't exist
|
74
|
+
fk = foreign_keys.detect { |fk| fk.table_name == table_name.to_s && fk.column_names == Array(column_name).collect(&:to_s) }
|
75
|
+
remove_foreign_key(table_name, fk.name) if fk
|
45
76
|
end
|
46
77
|
|
78
|
+
|
47
79
|
def column_index(table_name, column_name, options) #:nodoc:
|
48
80
|
options = {} if options == true
|
49
81
|
options = { :unique => true } if options == :unique
|
@@ -51,5 +83,14 @@ module SchemaPlus::ActiveRecord
|
|
51
83
|
add_index(table_name, column_name, options)
|
52
84
|
end
|
53
85
|
|
86
|
+
def remove_auto_index_if_exists(table_name, column_name)
|
87
|
+
name = auto_index_name(table_name, column_name)
|
88
|
+
remove_index(table_name, :name => name) if index_exists?(table_name, column_name, :name => name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def auto_index_name(table_name, column_name)
|
92
|
+
ConnectionAdapters::ForeignKeyDefinition.auto_index_name(table_name, column_name)
|
93
|
+
end
|
94
|
+
|
54
95
|
end
|
55
96
|
end
|
@@ -35,14 +35,6 @@ module SchemaPlus
|
|
35
35
|
adapter_module = SchemaPlus::ActiveRecord::ConnectionAdapters.const_get(adapter)
|
36
36
|
self.class.send(:include, adapter_module) unless self.class.include?(adapter_module)
|
37
37
|
self.post_initialize if self.respond_to? :post_initialize
|
38
|
-
# rails 3.1 defines a separate Mysql2IndexDefinition which is
|
39
|
-
# compatible with the monkey patches; but the definition only
|
40
|
-
# appears once the adapter is loaded. so wait til now to check
|
41
|
-
# if that constant exists, then include the patches
|
42
|
-
if mysql2index = ::ActiveRecord::ConnectionAdapters::Mysql2IndexDefinition rescue nil # rescues NameError
|
43
|
-
monkeypatch = SchemaPlus::ActiveRecord::ConnectionAdapters::IndexDefinition
|
44
|
-
mysql2index.send(:include, monkeypatch) unless mysql2index.include? monkeypatch
|
45
|
-
end
|
46
38
|
|
47
39
|
if adapter == 'PostgresqlAdapter'
|
48
40
|
::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.send(:include, SchemaPlus::ActiveRecord::ConnectionAdapters::PostgreSQLColumn) unless ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.include?(SchemaPlus::ActiveRecord::ConnectionAdapters::PostgreSQLColumn)
|
@@ -57,6 +49,7 @@ module SchemaPlus
|
|
57
49
|
# Create a view given the SQL definition. Specify :force => true
|
58
50
|
# to first drop the view if it already exists.
|
59
51
|
def create_view(view_name, definition, options={})
|
52
|
+
definition = definition.to_sql if definition.respond_to? :to_sql
|
60
53
|
execute "DROP VIEW IF EXISTS #{quote_table_name(view_name)}" if options[:force]
|
61
54
|
execute "CREATE VIEW #{quote_table_name(view_name)} AS #{definition}"
|
62
55
|
end
|
@@ -66,21 +59,6 @@ module SchemaPlus
|
|
66
59
|
execute "DROP VIEW #{quote_table_name(view_name)}"
|
67
60
|
end
|
68
61
|
|
69
|
-
#--
|
70
|
-
# these are all expected to be defined by subclasses, listing them
|
71
|
-
# here only as templates.
|
72
|
-
#++
|
73
|
-
# Returns a list of all views (abstract)
|
74
|
-
def views(name = nil) [] end
|
75
|
-
# Returns the SQL definition of a given view (abstract)
|
76
|
-
def view_definition(view_name, name = nil) end
|
77
|
-
# Return the ForeignKeyDefinition objects for foreign key
|
78
|
-
# constraints defined on this table (abstract)
|
79
|
-
def foreign_keys(table_name, name = nil) [] end
|
80
|
-
# Return the ForeignKeyDefinition objects for foreign key
|
81
|
-
# constraints defined on other tables that reference this table
|
82
|
-
# (abstract)
|
83
|
-
def reverse_foreign_keys(table_name, name = nil) [] end
|
84
62
|
|
85
63
|
# Define a foreign key constraint. Valid options are :on_update,
|
86
64
|
# :on_delete, and :deferrable, with values as described at
|
@@ -117,6 +95,31 @@ module SchemaPlus
|
|
117
95
|
drop_table_without_schema_plus(name)
|
118
96
|
end
|
119
97
|
|
98
|
+
# called from individual adpaters, after renaming table from old
|
99
|
+
# name to
|
100
|
+
def rename_indexes_and_foreign_keys(oldname, newname) #:nodoc:
|
101
|
+
indexes(newname).select{|index| index.name == index_name(oldname, index.columns)}.each do |index|
|
102
|
+
rename_index(newname, index.name, index_name(newname, index.columns))
|
103
|
+
end
|
104
|
+
foreign_keys(newname).each do |fk|
|
105
|
+
index = indexes(newname).find{|index| index.name == ForeignKeyDefinition.auto_index_name(oldname, index.columns)}
|
106
|
+
begin
|
107
|
+
remove_foreign_key(newname, fk.name)
|
108
|
+
rescue NotImplementedError
|
109
|
+
# sqlite3 can't remove foreign keys, so just skip it
|
110
|
+
end
|
111
|
+
# rename the index only when the fk constraint doesn't exist.
|
112
|
+
# mysql doesn't allow the rename (which is a delete & add)
|
113
|
+
# if the index is on a foreign key constraint
|
114
|
+
rename_index(newname, index.name, ForeignKeyDefinition.auto_index_name(newname, index.columns)) if index
|
115
|
+
begin
|
116
|
+
add_foreign_key(newname, fk.column_names, fk.references_table_name, fk.references_column_names, :name => newname, :on_update => fk.on_update, :on_delete => fk.on_delete, :deferrable => fk.deferrable)
|
117
|
+
rescue NotImplementedError
|
118
|
+
# sqlite3 can't add foreign keys, so just skip it
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
120
123
|
# Returns true if the database supports parital indexes (abstract; only
|
121
124
|
# Postgresql returns true)
|
122
125
|
def supports_partial_indexes?
|
@@ -146,14 +149,6 @@ module SchemaPlus
|
|
146
149
|
end
|
147
150
|
end
|
148
151
|
|
149
|
-
def default_expr_valid?(expr)
|
150
|
-
# override in database specific adaptor
|
151
|
-
end
|
152
|
-
|
153
|
-
def sql_for_function(function_name)
|
154
|
-
# override in database specific adaptor
|
155
|
-
end
|
156
|
-
|
157
152
|
# This is define in rails 3.x, but not in rails2.x
|
158
153
|
unless defined? ::ActiveRecord::ConnectionAdapters::SchemaStatements::index_name_exists?
|
159
154
|
# File activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb, line 403
|
@@ -163,6 +158,40 @@ module SchemaPlus
|
|
163
158
|
indexes(table_name).detect { |i| i.name == index_name }
|
164
159
|
end
|
165
160
|
end
|
161
|
+
|
162
|
+
#####################################################################
|
163
|
+
#
|
164
|
+
# The functions below here are abstract; each subclass should
|
165
|
+
# define them all. Defining them here only for reference.
|
166
|
+
#
|
167
|
+
|
168
|
+
# (abstract) Returns the names of all views, as an array of strings
|
169
|
+
def views(name = nil) raise "Internal Error: Connection adapter didn't override abstract function"; [] end
|
170
|
+
|
171
|
+
# (abstract) Returns the SQL definition of a given view. This is
|
172
|
+
# the literal SQL would come after 'CREATVE VIEW viewname AS ' in
|
173
|
+
# the SQL statement to create a view.
|
174
|
+
def view_definition(view_name, name = nil) raise "Internal Error: Connection adapter didn't override abstract function"; end
|
175
|
+
|
176
|
+
# (abstract) Return the ForeignKeyDefinition objects for foreign key
|
177
|
+
# constraints defined on this table
|
178
|
+
def foreign_keys(table_name, name = nil) raise "Internal Error: Connection adapter didn't override abstract function"; [] end
|
179
|
+
|
180
|
+
# (abstract) Return the ForeignKeyDefinition objects for foreign key
|
181
|
+
# constraints defined on other tables that reference this table
|
182
|
+
def reverse_foreign_keys(table_name, name = nil) raise "Internal Error: Connection adapter didn't override abstract function"; [] end
|
183
|
+
|
184
|
+
# (abstract) Return true if the passed expression can be used as a column
|
185
|
+
# default value. (For most databases the specific expression
|
186
|
+
# doesn't matter, and the adapter's function would return a
|
187
|
+
# constant true if default expressions are supported or false if
|
188
|
+
# they're not.)
|
189
|
+
def default_expr_valid?(expr) raise "Internal Error: Connection adapter didn't override abstract function"; end
|
190
|
+
|
191
|
+
# (abstract) Return SQL definition for a given canonical function_name symbol.
|
192
|
+
# Currently, the only function to support is :now, which should
|
193
|
+
# return a DATETIME object for the current time.
|
194
|
+
def sql_for_function(function_name) raise "Internal Error: Connection adapter didn't override abstract function"; end
|
166
195
|
|
167
196
|
end
|
168
197
|
end
|
@@ -31,7 +31,7 @@ module SchemaPlus
|
|
31
31
|
# possible values.
|
32
32
|
attr_reader :on_update
|
33
33
|
|
34
|
-
# The
|
34
|
+
# The ON_DELETE behavior for the constraint. See above for the
|
35
35
|
# possible values.
|
36
36
|
attr_reader :on_delete
|
37
37
|
|
@@ -39,7 +39,7 @@ module SchemaPlus
|
|
39
39
|
attr_reader :deferrable
|
40
40
|
|
41
41
|
# :enddoc:
|
42
|
-
|
42
|
+
|
43
43
|
ACTIONS = { :cascade => "CASCADE", :restrict => "RESTRICT", :set_null => "SET NULL", :set_default => "SET DEFAULT", :no_action => "NO ACTION" }.freeze
|
44
44
|
|
45
45
|
def initialize(name, table_name, column_names, references_table_name, references_column_names, on_update = nil, on_delete = nil, deferrable = nil)
|
@@ -105,6 +105,11 @@ module SchemaPlus
|
|
105
105
|
def __unquote(value)
|
106
106
|
value.to_s.sub(/^["`](.*)["`]$/, '\1')
|
107
107
|
end
|
108
|
+
|
109
|
+
def self.auto_index_name(table_name, column_name)
|
110
|
+
"fk__#{table_name}_#{Array.wrap(column_name).join('_and_')}"
|
111
|
+
end
|
112
|
+
|
108
113
|
end
|
109
114
|
end
|
110
115
|
end
|
@@ -22,7 +22,7 @@ module SchemaPlus
|
|
22
22
|
# same args as add_index(table_name, column_names, options)
|
23
23
|
if args.length == 3 and Hash === args.last
|
24
24
|
table_name, column_names, options = args + [{}]
|
25
|
-
initialize_without_schema_plus(table_name, options[:name], options[:unique], column_names, options[:lengths])
|
25
|
+
initialize_without_schema_plus(table_name, options[:name], options[:unique], column_names, options[:lengths], options[:orders])
|
26
26
|
@conditions = options[:conditions]
|
27
27
|
@expression = options[:expression]
|
28
28
|
@kind = options[:kind]
|
@@ -48,6 +48,7 @@ module SchemaPlus
|
|
48
48
|
|
49
49
|
# tests if the corresponding indexes would be the same
|
50
50
|
def ==(other)
|
51
|
+
return false if other.nil?
|
51
52
|
return false unless self.name == other.name
|
52
53
|
return false unless Array.wrap(self.columns).collect(&:to_s).sort == Array.wrap(other.columns).collect(&:to_s).sort
|
53
54
|
return false unless !!self.unique == !!other.unique
|
@@ -12,6 +12,8 @@ module SchemaPlus
|
|
12
12
|
base.class_eval do
|
13
13
|
alias_method_chain :tables, :schema_plus
|
14
14
|
alias_method_chain :remove_column, :schema_plus
|
15
|
+
alias_method_chain :rename_table, :schema_plus
|
16
|
+
alias_method_chain :exec_stmt, :schema_plus rescue nil # only defined for mysql not mysql2
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
@@ -26,11 +28,27 @@ module SchemaPlus
|
|
26
28
|
remove_column_without_schema_plus(table_name, column_name)
|
27
29
|
end
|
28
30
|
|
31
|
+
def rename_table_with_schema_plus(oldname, newname)
|
32
|
+
rename_table_without_schema_plus(oldname, newname)
|
33
|
+
rename_indexes_and_foreign_keys(oldname, newname)
|
34
|
+
end
|
35
|
+
|
36
|
+
def exec_stmt_with_schema_plus(sql, name, binds, &block)
|
37
|
+
if binds.any?{ |col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
|
38
|
+
binds.each_with_index do |(col, val), i|
|
39
|
+
if val.equal? ::ActiveRecord::DB_DEFAULT
|
40
|
+
sql = sql.sub(/(([^?]*?){#{i}}[^?]*)\?/, "\\1DEFAULT")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
binds = binds.reject{|col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
|
44
|
+
end
|
45
|
+
exec_stmt_without_schema_plus(sql, name, binds, &block)
|
46
|
+
end
|
47
|
+
|
29
48
|
def remove_foreign_key(table_name, foreign_key_name, options = {})
|
30
49
|
execute "ALTER TABLE #{quote_table_name(table_name)} DROP FOREIGN KEY #{foreign_key_name}"
|
31
50
|
end
|
32
51
|
|
33
|
-
|
34
52
|
def foreign_keys(table_name, name = nil)
|
35
53
|
results = execute("SHOW CREATE TABLE #{quote_table_name(table_name)}", name)
|
36
54
|
|
@@ -25,10 +25,10 @@ module SchemaPlus
|
|
25
25
|
|
26
26
|
module ClassMethods
|
27
27
|
def extract_value_from_default_with_schema_plus(default)
|
28
|
-
|
28
|
+
|
29
29
|
|
30
30
|
value = extract_value_from_default_without_schema_plus(default)
|
31
|
-
|
31
|
+
|
32
32
|
# in some cases (e.g. if change_column_default(table, column,
|
33
33
|
# nil) is used), postgresql will return NULL::xxxxx (rather
|
34
34
|
# than nil) for a null default -- make sure we treat it as nil,
|
@@ -50,6 +50,8 @@ module SchemaPlus
|
|
50
50
|
def self.included(base) #:nodoc:
|
51
51
|
base.class_eval do
|
52
52
|
remove_method :indexes
|
53
|
+
alias_method_chain :rename_table, :schema_plus
|
54
|
+
alias_method_chain :exec_cache, :schema_plus
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
@@ -74,13 +76,24 @@ module SchemaPlus
|
|
74
76
|
index_type = options[:unique] ? "UNIQUE" : ""
|
75
77
|
index_name = options[:name] || index_name(table_name, column_names)
|
76
78
|
conditions = options[:conditions]
|
79
|
+
kind = options[:kind]
|
77
80
|
|
78
|
-
if options[:expression] then
|
79
|
-
|
81
|
+
if expression = options[:expression] then
|
82
|
+
# Wrap expression in parentheses if necessary
|
83
|
+
expression = "(#{expression})" if expression !~ /(using|with|tablespace|where)/i
|
84
|
+
expression = "USING #{kind} #{expression}" if kind
|
85
|
+
expression = "#{expression} WHERE #{conditions}" if conditions
|
86
|
+
|
87
|
+
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
|
80
88
|
else
|
81
|
-
|
89
|
+
option_strings = Hash[column_names.map {|name| [name, '']}]
|
90
|
+
option_strings = add_index_sort_order(option_strings, column_names, options)
|
91
|
+
|
92
|
+
quoted_column_names = column_names.map { |e| (options[:case_sensitive] == false && e.to_s !~ /_id$/ ? "LOWER(#{quote_column_name(e)})" : quote_column_name(e)) + option_strings[e] }
|
93
|
+
expression = "(#{quoted_column_names.join(', ')})"
|
94
|
+
expression = "USING #{kind} #{expression}" if kind
|
82
95
|
|
83
|
-
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}
|
96
|
+
sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{expression}"
|
84
97
|
sql += " WHERE (#{ ::ActiveRecord::Base.send(:sanitize_sql, conditions, quote_table_name(table_name)) })" if conditions
|
85
98
|
end
|
86
99
|
execute sql
|
@@ -92,23 +105,28 @@ module SchemaPlus
|
|
92
105
|
true
|
93
106
|
end
|
94
107
|
|
108
|
+
# This method entirely duplicated from AR's postgresql_adapter.c,
|
109
|
+
# but includes the extra bit to determine the column name for a
|
110
|
+
# case-insensitive index. (Haven't come up with any clever way to
|
111
|
+
# only code up the case-insensitive column name bit here and
|
112
|
+
# otherwise use the existing method.)
|
95
113
|
def indexes(table_name, name = nil) #:nodoc:
|
96
|
-
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
97
114
|
result = query(<<-SQL, name)
|
98
|
-
|
99
|
-
|
100
|
-
|
115
|
+
|
116
|
+
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
|
117
|
+
m.amname, pg_get_expr(d.indpred, t.oid), pg_get_expr(d.indexprs, t.oid)
|
118
|
+
FROM pg_class t
|
119
|
+
INNER JOIN pg_index d ON t.oid = d.indrelid
|
120
|
+
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
121
|
+
INNER JOIN pg_am m ON i.relam = m.oid
|
101
122
|
WHERE i.relkind = 'i'
|
102
|
-
AND i.relam = m.oid
|
103
|
-
AND d.indexrelid = i.oid
|
104
123
|
AND d.indisprimary = 'f'
|
105
|
-
AND t.oid = d.indrelid
|
106
124
|
AND t.relname = '#{table_name}'
|
107
|
-
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname
|
125
|
+
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
108
126
|
ORDER BY i.relname
|
109
127
|
SQL
|
110
128
|
|
111
|
-
result.map do |(index_name, is_unique, indkey,
|
129
|
+
result.map do |(index_name, is_unique, indkey, inddef, oid, kind, conditions, expression)|
|
112
130
|
unique = (is_unique == 't')
|
113
131
|
index_keys = indkey.split(" ")
|
114
132
|
|
@@ -120,12 +138,20 @@ module SchemaPlus
|
|
120
138
|
SQL
|
121
139
|
|
122
140
|
column_names = columns.values_at(*index_keys).compact
|
141
|
+
# extract column name from the expression, for a
|
142
|
+
# case-insensitive
|
123
143
|
if md = expression.try(:match, /^lower\(\(?([^)]+)\)?(::text)?\)$/i)
|
124
144
|
column_names << md[1]
|
125
145
|
end
|
146
|
+
|
147
|
+
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
148
|
+
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
149
|
+
orders = desc_order_columns.any? ? Hash[column_names.map {|column| [column, desc_order_columns.include?(column) ? :desc : :asc]}] : {}
|
150
|
+
|
126
151
|
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, column_names,
|
127
152
|
:name => index_name,
|
128
153
|
:unique => unique,
|
154
|
+
:orders => orders,
|
129
155
|
:conditions => conditions,
|
130
156
|
:case_sensitive => !(expression =~ /lower/i),
|
131
157
|
:kind => kind.downcase == "btree" ? nil : kind,
|
@@ -133,6 +159,30 @@ module SchemaPlus
|
|
133
159
|
end
|
134
160
|
end
|
135
161
|
|
162
|
+
def rename_table_with_schema_plus(oldname, newname) #:nodoc:
|
163
|
+
rename_table_without_schema_plus(oldname, newname)
|
164
|
+
rename_indexes_and_foreign_keys(oldname, newname)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Prepass to replace each ActiveRecord::DB_DEFAULT with a literal
|
168
|
+
# DEFAULT in the sql string. (The underlying pg gem provides no
|
169
|
+
# way to bind a value that will replace $n with DEFAULT)
|
170
|
+
def exec_cache_with_schema_plus(sql, binds)
|
171
|
+
if binds.any?{ |col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
|
172
|
+
j = 0
|
173
|
+
binds.each_with_index do |(col, val), i|
|
174
|
+
if val.equal? ::ActiveRecord::DB_DEFAULT
|
175
|
+
sql = sql.sub(/\$#{i+1}/, 'DEFAULT')
|
176
|
+
else
|
177
|
+
sql = sql.sub(/\$#{i+1}/, "$#{j+1}") if i != j
|
178
|
+
j += 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
binds = binds.reject{|col, val| val.equal? ::ActiveRecord::DB_DEFAULT}
|
182
|
+
end
|
183
|
+
exec_cache_without_schema_plus(sql, binds)
|
184
|
+
end
|
185
|
+
|
136
186
|
def foreign_keys(table_name, name = nil) #:nodoc:
|
137
187
|
load_foreign_keys(<<-SQL, name)
|
138
188
|
SELECT f.conname, pg_get_constraintdef(f.oid), t.relname
|
@@ -140,6 +190,7 @@ module SchemaPlus
|
|
140
190
|
WHERE f.conrelid = t.oid
|
141
191
|
AND f.contype = 'f'
|
142
192
|
AND t.relname = '#{table_name}'
|
193
|
+
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
143
194
|
SQL
|
144
195
|
end
|
145
196
|
|
@@ -151,15 +202,15 @@ module SchemaPlus
|
|
151
202
|
AND f.conrelid = t2.oid
|
152
203
|
AND f.contype = 'f'
|
153
204
|
AND t.relname = '#{table_name}'
|
205
|
+
AND t.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
154
206
|
SQL
|
155
207
|
end
|
156
208
|
|
157
209
|
def views(name = nil) #:nodoc:
|
158
|
-
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
159
210
|
query(<<-SQL, name).map { |row| row[0] }
|
160
211
|
SELECT viewname
|
161
212
|
FROM pg_views
|
162
|
-
WHERE schemaname
|
213
|
+
WHERE schemaname = ANY (current_schemas(false))
|
163
214
|
SQL
|
164
215
|
end
|
165
216
|
|