schema_plus 0.4.1 → 1.0.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.
Files changed (55) hide show
  1. data/.gitignore +3 -1
  2. data/.travis.yml +6 -9
  3. data/Gemfile +0 -4
  4. data/README.rdoc +168 -70
  5. data/Rakefile +58 -47
  6. data/gemfiles/rails-3.2/Gemfile.base +4 -0
  7. data/gemfiles/rails-3.2/Gemfile.mysql +4 -0
  8. data/gemfiles/rails-3.2/Gemfile.mysql2 +4 -0
  9. data/gemfiles/rails-3.2/Gemfile.postgresql +4 -0
  10. data/gemfiles/rails-3.2/Gemfile.sqlite3 +4 -0
  11. data/lib/schema_plus.rb +2 -0
  12. data/lib/schema_plus/active_record/column_options_handler.rb +73 -32
  13. data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +60 -31
  14. data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +7 -2
  15. data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +2 -1
  16. data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +19 -1
  17. data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +68 -17
  18. data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +28 -3
  19. data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +27 -1
  20. data/lib/schema_plus/active_record/db_default.rb +19 -0
  21. data/lib/schema_plus/active_record/foreign_keys.rb +40 -32
  22. data/lib/schema_plus/active_record/schema_dumper.rb +7 -3
  23. data/lib/schema_plus/version.rb +1 -1
  24. data/runspecs +5 -8
  25. data/schema_plus.gemspec +2 -5
  26. data/spec/column_definition_spec.rb +18 -1
  27. data/spec/column_spec.rb +39 -2
  28. data/spec/connection_spec.rb +1 -1
  29. data/spec/connections/mysql/connection.rb +1 -1
  30. data/spec/connections/mysql2/connection.rb +1 -1
  31. data/spec/connections/postgresql/connection.rb +1 -1
  32. data/spec/foreign_key_definition_spec.rb +0 -4
  33. data/spec/foreign_key_spec.rb +37 -13
  34. data/spec/index_definition_spec.rb +54 -2
  35. data/spec/index_spec.rb +59 -15
  36. data/spec/migration_spec.rb +336 -85
  37. data/spec/multiple_schemas_spec.rb +127 -0
  38. data/spec/schema_dumper_spec.rb +65 -25
  39. data/spec/schema_spec.rb +16 -18
  40. data/spec/spec_helper.rb +19 -18
  41. data/spec/support/matchers/reference.rb +7 -1
  42. data/spec/views_spec.rb +5 -2
  43. metadata +43 -54
  44. data/gemfiles/Gemfile.rails-2.3 +0 -6
  45. data/gemfiles/Gemfile.rails-2.3.lock +0 -65
  46. data/gemfiles/Gemfile.rails-3.0 +0 -5
  47. data/gemfiles/Gemfile.rails-3.0.lock +0 -113
  48. data/gemfiles/Gemfile.rails-3.1 +0 -5
  49. data/gemfiles/Gemfile.rails-3.1.lock +0 -123
  50. data/gemfiles/Gemfile.rails-3.2 +0 -5
  51. data/gemfiles/Gemfile.rails-3.2.lock +0 -121
  52. data/spec/models/comment.rb +0 -2
  53. data/spec/models/post.rb +0 -2
  54. data/spec/models/user.rb +0 -2
  55. data/spec/rails3_migration_spec.rb +0 -144
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gemspec :path => File.expand_path('../../..', __FILE__)
4
+ gem "rails", "~> 3.2.0"
@@ -0,0 +1,4 @@
1
+ require "pathname"
2
+ eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
3
+
4
+ gem "mysql", "~> 2.8.1"
@@ -0,0 +1,4 @@
1
+ require "pathname"
2
+ eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
3
+
4
+ gem "mysql2"
@@ -0,0 +1,4 @@
1
+ require "pathname"
2
+ eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
3
+
4
+ gem "pg"
@@ -0,0 +1,4 @@
1
+ require "pathname"
2
+ eval(Pathname.new(__FILE__).dirname.join("Gemfile.base").read, binding)
3
+
4
+ gem "sqlite3"
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
- if references = get_references(table_name, column_name, column_options, config)
6
- if index = column_options.fetch(:index, config.foreign_keys.auto_index?)
7
- column_index(table_name, column_name, index)
8
- end
9
- add_foreign_key(table_name, column_name, references.first, references.last,
10
- column_options.reverse_merge(:on_update => config.foreign_keys.on_update,
11
- :on_delete => config.foreign_keys.on_delete))
12
- elsif column_options[:index]
13
- column_index(table_name, column_name, column_options[:index])
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
- # If auto_create is true:
20
- # get_references('comments', 'post_id') # => ['posts', 'id']
21
- #
22
- # And if <tt>column_name</tt> is parent_id it references to the same table
23
- # get_references('pages', 'parent_id') # => ['pages', 'id']
24
- #
25
- # If :references option is given, it is used (whether or not auto_create is true)
26
- # get_references('widgets', 'main_page_id', :references => 'pages')) => ['pages', 'id']
27
- #
28
- # Also the referenced id column may be specified:
29
- # get_references('addresses', 'member_id', :references => ['users', 'uuid']) => ['users', 'uuid']
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
- references = [references, :id] unless references.nil? || references.is_a?(Array)
35
- references
36
- elsif config.foreign_keys.auto_create?
37
- case column_name.to_s
38
- when 'parent_id'
39
- [table_name, :id]
40
- when /^(.*)_id$/
41
- determined_table_name = ActiveRecord::Base.pluralize_table_names ? $1.to_s.pluralize : $1
42
- [determined_table_name, :id]
43
- end
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 ON_UPDATE behavior for the constraint. See above for 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
- sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{options[:expression]}"
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
- quoted_column_names = column_names.map { |e| options[:case_sensitive] == false && e.to_s !~ /_id$/ ? "LOWER(#{quote_column_name(e)})" : quote_column_name(e) }
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)} (#{quoted_column_names.join(", ")})"
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
- SELECT distinct i.relname, d.indisunique, d.indkey, m.amname, t.oid,
99
- pg_get_expr(d.indpred, t.oid), pg_get_expr(d.indexprs, t.oid)
100
- FROM pg_class t, pg_class i, pg_index d, pg_am m
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 IN (#{schemas}) )
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, kind, oid, conditions, expression)|
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 IN (#{schemas})
213
+ WHERE schemaname = ANY (current_schemas(false))
163
214
  SQL
164
215
  end
165
216