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.
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