schema_plus 0.1.0.pre1

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 +25 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +25 -0
  4. data/README.rdoc +147 -0
  5. data/Rakefile +70 -0
  6. data/init.rb +1 -0
  7. data/lib/schema_plus/active_record/associations.rb +211 -0
  8. data/lib/schema_plus/active_record/base.rb +81 -0
  9. data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +96 -0
  10. data/lib/schema_plus/active_record/connection_adapters/column.rb +55 -0
  11. data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +115 -0
  12. data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +51 -0
  13. data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +111 -0
  14. data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +163 -0
  15. data/lib/schema_plus/active_record/connection_adapters/schema_statements.rb +39 -0
  16. data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +78 -0
  17. data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +130 -0
  18. data/lib/schema_plus/active_record/migration.rb +220 -0
  19. data/lib/schema_plus/active_record/schema.rb +27 -0
  20. data/lib/schema_plus/active_record/schema_dumper.rb +122 -0
  21. data/lib/schema_plus/active_record/validations.rb +139 -0
  22. data/lib/schema_plus/railtie.rb +12 -0
  23. data/lib/schema_plus/version.rb +3 -0
  24. data/lib/schema_plus.rb +248 -0
  25. data/schema_plus.gemspec +37 -0
  26. data/schema_plus.gemspec.rails3.0 +36 -0
  27. data/schema_plus.gemspec.rails3.1 +36 -0
  28. data/spec/association_spec.rb +529 -0
  29. data/spec/connections/mysql/connection.rb +18 -0
  30. data/spec/connections/mysql2/connection.rb +18 -0
  31. data/spec/connections/postgresql/connection.rb +15 -0
  32. data/spec/connections/sqlite3/connection.rb +14 -0
  33. data/spec/foreign_key_definition_spec.rb +23 -0
  34. data/spec/foreign_key_spec.rb +142 -0
  35. data/spec/index_definition_spec.rb +139 -0
  36. data/spec/index_spec.rb +71 -0
  37. data/spec/migration_spec.rb +405 -0
  38. data/spec/models/comment.rb +2 -0
  39. data/spec/models/post.rb +2 -0
  40. data/spec/models/user.rb +2 -0
  41. data/spec/references_spec.rb +78 -0
  42. data/spec/schema/auto_schema.rb +23 -0
  43. data/spec/schema/core_schema.rb +21 -0
  44. data/spec/schema_dumper_spec.rb +167 -0
  45. data/spec/schema_spec.rb +71 -0
  46. data/spec/spec_helper.rb +59 -0
  47. data/spec/support/extensions/active_model.rb +13 -0
  48. data/spec/support/helpers.rb +16 -0
  49. data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
  50. data/spec/support/matchers/have_index.rb +52 -0
  51. data/spec/support/matchers/reference.rb +66 -0
  52. data/spec/support/reference.rb +66 -0
  53. data/spec/validations_spec.rb +294 -0
  54. data/spec/views_spec.rb +140 -0
  55. metadata +269 -0
@@ -0,0 +1,81 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+
4
+ #
5
+ # SchemaPlus adds several methods to ActiveRecord::Base
6
+ #
7
+ module Base
8
+ def self.included(base) #:nodoc:
9
+ base.extend(ClassMethods)
10
+ base.extend(SchemaPlus::ActiveRecord::Associations)
11
+ base.extend(SchemaPlus::ActiveRecord::Validations)
12
+ end
13
+
14
+ module ClassMethods #:nodoc:
15
+ def self.extended(base) #:nodoc:
16
+ class << base
17
+ alias_method_chain :columns, :schema_plus
18
+ alias_method_chain :abstract_class?, :schema_plus
19
+ alias_method_chain :reset_column_information, :schema_plus
20
+ end
21
+ end
22
+
23
+ public
24
+
25
+ # Per-model override of Config options. Use via, e.g.
26
+ # class MyModel < ActiveRecord::Base
27
+ # schema_plus :associations => { :auto_create => false }
28
+ # end
29
+ def schema_plus(opts)
30
+ @schema_plus_config = SchemaPlus.config.merge(opts)
31
+ end
32
+
33
+ def abstract_class_with_schema_plus? #:nodoc:
34
+ abstract_class_without_schema_plus? || !(name =~ /^Abstract/).nil?
35
+ end
36
+
37
+ def columns_with_schema_plus #:nodoc:
38
+ unless @schema_plus_extended_columns
39
+ @schema_plus_extended_columns = true
40
+ cols = columns_hash
41
+ indexes.each do |index|
42
+ index.columns.each do |name|
43
+ cols[name].indexes << index
44
+ end
45
+ end
46
+ end
47
+ columns_without_schema_plus
48
+ end
49
+
50
+ def reset_column_information_with_schema_plus #:nodoc:
51
+ reset_column_information_without_schema_plus
52
+ @indexes = @foreign_keys = @schema_plus_extended_columns = nil
53
+ end
54
+
55
+ # Returns a list of IndexDefinition objects, for each index
56
+ # defind on this model's table.
57
+ def indexes
58
+ @indexes ||= connection.indexes(table_name, "#{name} Indexes")
59
+ end
60
+
61
+ # Returns a list of ForeignKeyDefinition objects, for each foreign
62
+ # key constraint defined in this model's table
63
+ def foreign_keys
64
+ @foreign_keys ||= connection.foreign_keys(table_name, "#{name} Foreign Keys")
65
+ end
66
+
67
+ # Returns a list of ForeignKeyDefinition objects, for each foreign
68
+ # key constraint of other tables that refer to this model's table
69
+ def reverse_foreign_keys
70
+ connection.reverse_foreign_keys(table_name, "#{name} Reverse Foreign Keys")
71
+ end
72
+
73
+ private
74
+
75
+ def schema_plus_config # :nodoc:
76
+ @schema_plus_config ||= SchemaPlus.config.dup
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,96 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ # SchemaPlus adds several methods to the connection adapter (as returned by ActiveRecordBase#connection). See AbstractAdapter for details.
4
+ module ConnectionAdapters
5
+
6
+ #
7
+ # SchemaPlus adds several methods to
8
+ # ActiveRecord::ConnectionAdapters::AbstractAdapter. In most cases
9
+ # you don't call these directly, but rather the methods that define
10
+ # things are called by schema statements, and methods that query
11
+ # things are called by ActiveRecord::Base.
12
+ #
13
+ module AbstractAdapter
14
+ def self.included(base) #:nodoc:
15
+ base.alias_method_chain :initialize, :schema_plus
16
+ base.alias_method_chain :drop_table, :schema_plus
17
+ end
18
+
19
+ def initialize_with_schema_plus(*args) #:nodoc:
20
+ initialize_without_schema_plus(*args)
21
+ adapter = nil
22
+ case adapter_name
23
+ # name of MySQL adapter depends on mysql gem
24
+ # * with mysql gem adapter is named MySQL
25
+ # * with mysql2 gem adapter is named Mysql2
26
+ # Here we handle this and hopefully futher adapter names
27
+ when /^MySQL/i
28
+ adapter = 'MysqlAdapter'
29
+ when 'PostgreSQL'
30
+ adapter = 'PostgresqlAdapter'
31
+ when 'SQLite'
32
+ adapter = 'Sqlite3Adapter'
33
+ end
34
+ if adapter
35
+ adapter_module = SchemaPlus::ActiveRecord::ConnectionAdapters.const_get(adapter)
36
+ self.class.send(:include, adapter_module) unless self.class.include?(adapter_module)
37
+ self.post_initialize if self.respond_to? :post_initialize
38
+ end
39
+ end
40
+
41
+ # Create a view given the SQL definition
42
+ def create_view(view_name, definition)
43
+ execute "CREATE VIEW #{quote_table_name(view_name)} AS #{definition}"
44
+ end
45
+
46
+ # Drop the named view
47
+ def drop_view(view_name)
48
+ execute "DROP VIEW #{quote_table_name(view_name)}"
49
+ end
50
+
51
+ #--
52
+ # these are all expected to be defined by subclasses, listing them
53
+ # here only as templates.
54
+ #++
55
+ # Returns a list of all views (abstract)
56
+ def views(name = nil) [] end
57
+ # Returns the SQL definition of a given view (abstract)
58
+ def view_definition(view_name, name = nil) end
59
+ # Return the ForeignKeyDefinition objects for foreign key
60
+ # constraints defined on this table (abstract)
61
+ def foreign_keys(table_name, name = nil) [] end
62
+ # Return the ForeignKeyDefinition objects for foreign key
63
+ # constraints defined on other tables that reference this table
64
+ # (abstract)
65
+ def reverse_foreign_keys(table_name, name = nil) [] end
66
+
67
+ # Define a foreign key constraint. Valid options are :on_update,
68
+ # :on_delete, and :deferrable, with values as described at
69
+ # ForeignKeyDefinition
70
+ def add_foreign_key(table_name, column_names, references_table_name, references_column_names, options = {})
71
+ foreign_key = ForeignKeyDefinition.new(options[:name], table_name, column_names, ::ActiveRecord::Migrator.proper_table_name(references_table_name), references_column_names, options[:on_update], options[:on_delete], options[:deferrable])
72
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{foreign_key.to_sql}"
73
+ end
74
+
75
+ # Remove a foreign key constraint
76
+ def remove_foreign_key(table_name, foreign_key_name)
77
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{foreign_key_name}"
78
+ end
79
+
80
+ def drop_table_with_schema_plus(name, options = {}) #:nodoc:
81
+ unless ::ActiveRecord::Base.connection.class.include?(SchemaPlus::ActiveRecord::ConnectionAdapters::Sqlite3Adapter)
82
+ reverse_foreign_keys(name).each { |foreign_key| remove_foreign_key(foreign_key.table_name, foreign_key.name) }
83
+ end
84
+ drop_table_without_schema_plus(name, options)
85
+ end
86
+
87
+ # Returns true if the database supports parital indexes (abstract; only
88
+ # Postgresql returns true)
89
+ def supports_partial_indexes?
90
+ false
91
+ end
92
+
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,55 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+
5
+ #
6
+ # SchemaPlus adds several methods to Column
7
+ #
8
+ module Column
9
+
10
+ # Returns the list of IndexDefinition instances for each index that
11
+ # refers to this column. Returns an empty list if there are no
12
+ # such indexes.
13
+ def indexes
14
+ # list get filled by SchemaPlus::ActiveRecord::Base::columns_with_schema_plus
15
+ @indexes ||= []
16
+ end
17
+
18
+ # If the column is in a unique index, returns a list of names of other columns in
19
+ # the index. Returns an empty list if it's a single-column index.
20
+ # Returns nil if the column is not in a unique index.
21
+ def unique_scope
22
+ if index = indexes.select{|i| i.unique}.sort_by{|i| i.columns.size}.first
23
+ index.columns.reject{|name| name == self.name}
24
+ end
25
+ end
26
+
27
+ # Returns true if the column is in a unique index. See also
28
+ # unique_scope
29
+ def unique?
30
+ indexes.any?{|i| i.unique}
31
+ end
32
+
33
+ # Returns true if the column is in one or more indexes that are
34
+ # case sensitive
35
+ def case_sensitive?
36
+ indexes.any?{|i| i.case_sensitive?}
37
+ end
38
+
39
+ # Returns the circumstance in which the column must have a value:
40
+ # nil if the column may be null
41
+ # :save if the column has no default value
42
+ # :update otherwise
43
+ def required_on
44
+ if null
45
+ nil
46
+ elsif default.nil?
47
+ :save
48
+ else
49
+ :update
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,115 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ # Instances of this class are returned by the queries ActiveRecord::Base#foreign_keys and ActiveRecord::Base#reverse_foreign_keys (via AbstractAdapter#foreign_keys and AbstractAdapter#reverse_foreign_keys)
5
+ #
6
+ # The on_update and on_delete attributes can take on the following values:
7
+ # :cascade
8
+ # :restrict
9
+ # :set_null
10
+ # :set_default
11
+ # :no_action
12
+ class ForeignKeyDefinition
13
+
14
+ # The name of the foreign key constraint
15
+ attr_reader :name
16
+
17
+ # The name of the table the constraint is defined on
18
+ attr_reader :table_name
19
+
20
+ # The list of column names that are constrained (as strings).
21
+ attr_reader :column_names
22
+
23
+ # The foreign table that is referenced by the constraint
24
+ attr_reader :references_table_name
25
+
26
+ # The list of column names (as strings) of the foreign table that are referenced
27
+ # by the constraint
28
+ attr_reader :references_column_names
29
+
30
+ # The ON_UPDATE behavior for the constraint. See above for the
31
+ # possible values.
32
+ attr_reader :on_update
33
+
34
+ # The ON_UPDATE behavior for the constraint. See above for the
35
+ # possible values.
36
+ attr_reader :on_delete
37
+
38
+ # True if the constraint is deferrable
39
+ attr_reader :deferrable
40
+
41
+ # :enddoc:
42
+
43
+ ACTIONS = { :cascade => "CASCADE", :restrict => "RESTRICT", :set_null => "SET NULL", :set_default => "SET DEFAULT", :no_action => "NO ACTION" }.freeze
44
+
45
+ def initialize(name, table_name, column_names, references_table_name, references_column_names, on_update = nil, on_delete = nil, deferrable = nil)
46
+ @name = name
47
+ @table_name = unquote(table_name)
48
+ @column_names = unquote(column_names)
49
+ @references_table_name = unquote(references_table_name)
50
+ @references_column_names = unquote(references_column_names)
51
+ @on_update = on_update
52
+ @on_delete = on_delete
53
+ @deferrable = deferrable
54
+
55
+ ACTIONS.has_key?(on_update) or raise(ArgumentError, "invalid :on_update action: #{on_update.inspect}") if on_update
56
+ ACTIONS.has_key?(on_delete) or raise(ArgumentError, "invalid :on_delete action: #{on_delete.inspect}") if on_delete
57
+ if ::ActiveRecord::Base.connection.adapter_name =~ /^mysql/i
58
+ raise(NotImplementedError, "MySQL does not support ON UPDATE SET DEFAULT") if on_update == :set_default
59
+ raise(NotImplementedError, "MySQL does not support ON DELETE SET DEFAULT") if on_delete == :set_default
60
+ end
61
+ end
62
+
63
+ # Dumps a definition of foreign key.
64
+ # Must be invoked inside create_table block.
65
+ #
66
+ # It was introduced to satisfy sqlite which requires foreign key definitions
67
+ # to be declared when creating a table. That approach is fine for MySQL and
68
+ # PostgreSQL too.
69
+ def to_dump
70
+ dump = " t.foreign_key"
71
+ dump << " [#{Array(column_names).collect{ |name| name.inspect }.join(', ')}]"
72
+ dump << ", #{references_table_name.inspect}, [#{Array(references_column_names).collect{ |name| name.inspect }.join(', ')}]"
73
+ dump << ", :on_update => :#{on_update}" if on_update
74
+ dump << ", :on_delete => :#{on_delete}" if on_delete
75
+ dump << ", :deferrable => #{deferrable}" if deferrable
76
+ dump << ", :name => #{name.inspect}" if name
77
+ dump
78
+ end
79
+
80
+ def to_sql
81
+ sql = name ? "CONSTRAINT #{name} " : ""
82
+ sql << "FOREIGN KEY (#{quoted_column_names.join(", ")}) REFERENCES #{quoted_references_table_name} (#{quoted_references_column_names.join(", ")})"
83
+ sql << " ON UPDATE #{ACTIONS[on_update]}" if on_update
84
+ sql << " ON DELETE #{ACTIONS[on_delete]}" if on_delete
85
+ sql << " DEFERRABLE" if deferrable
86
+ sql
87
+ end
88
+
89
+ def quoted_column_names
90
+ Array(column_names).collect { |name| ::ActiveRecord::Base.connection.quote_column_name(name) }
91
+ end
92
+
93
+ def quoted_references_column_names
94
+ Array(references_column_names).collect { |name| ::ActiveRecord::Base.connection.quote_column_name(name) }
95
+ end
96
+
97
+ def quoted_references_table_name
98
+ ::ActiveRecord::Base.connection.quote_table_name(references_table_name)
99
+ end
100
+
101
+ def unquote(names)
102
+ if names.is_a?(Array)
103
+ names.collect { |name| __unquote(name) }
104
+ else
105
+ __unquote(names)
106
+ end
107
+ end
108
+
109
+ def __unquote(value)
110
+ value.to_s.sub(/^["`](.*)["`]$/, '\1')
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,51 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ #
5
+ # SchemaPlus extends the IndexDefinition object to return information
6
+ # about partial indexes and case sensitivity (i.e. Postgresql
7
+ # support).
8
+ module IndexDefinition
9
+ def self.included(base) #:nodoc:
10
+ base.alias_method_chain :initialize, :schema_plus
11
+ end
12
+
13
+ attr_reader :conditions
14
+ attr_reader :expression
15
+ attr_reader :kind
16
+
17
+ def case_sensitive?
18
+ @case_sensitive
19
+ end
20
+
21
+ def initialize_with_schema_plus(*args) #:nodoc:
22
+ # same args as add_index(table_name, column_names, options={})
23
+ if args.length == 2 or (args.length == 3 && Hash === args.last)
24
+ table_name, column_names, options = args + [{}]
25
+ initialize_without_schema_plus(table_name, options[:name], options[:unique], column_names, options[:lengths])
26
+ @conditions = options[:conditions]
27
+ @expression = options[:expression]
28
+ @kind = options[:kind]
29
+ @case_sensitive = options.include?(:case_sensitive) ? options[:case_sensitive] : true
30
+ else # backwards compatibility
31
+ initialize_without_schema_plus(*args)
32
+ @case_sensitive = true
33
+ end
34
+ end
35
+
36
+ # returns the options as a hash suitable for add_index
37
+ def opts #:nodoc:
38
+ opts = {}
39
+ opts[:name] = name unless name.nil?
40
+ opts[:unique] = unique unless unique.nil?
41
+ opts[:lengths] = lengths unless lengths.nil?
42
+ opts[:conditions] = conditions unless conditions.nil?
43
+ opts[:expression] = expression unless expression.nil?
44
+ opts[:kind] = kind unless kind.nil?
45
+ opts[:case_sensitive] = case_sensitive? unless @case_sensitive.nil?
46
+ opts
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,111 @@
1
+ module SchemaPlus
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ # SchemaPlus includes a MySQL implementation of the AbstractAdapater
5
+ # extensions. (This works with both the <tt>mysql</t> and
6
+ # <tt>mysql2</tt> gems.)
7
+ module MysqlAdapter
8
+
9
+ #:enddoc:
10
+
11
+ def self.included(base)
12
+ base.class_eval do
13
+ alias_method_chain :tables, :schema_plus
14
+ alias_method_chain :remove_column, :schema_plus
15
+ end
16
+ end
17
+
18
+ def tables_with_schema_plus(name=nil, *args)
19
+ tables_without_schema_plus(name, *args) - views(name)
20
+ end
21
+
22
+ def remove_column_with_schema_plus(table_name, column_name)
23
+ foreign_keys(table_name).select { |foreign_key| foreign_key.column_names.include?(column_name.to_s) }.each do |foreign_key|
24
+ remove_foreign_key(table_name, foreign_key.name)
25
+ end
26
+ remove_column_without_schema_plus(table_name, column_name)
27
+ end
28
+
29
+ def remove_foreign_key(table_name, foreign_key_name, options = {})
30
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP FOREIGN KEY #{foreign_key_name}"
31
+ end
32
+
33
+
34
+ def foreign_keys(table_name, name = nil)
35
+ results = execute("SHOW CREATE TABLE #{quote_table_name(table_name)}", name)
36
+
37
+ foreign_keys = []
38
+
39
+ results.each do |row|
40
+ row[1].lines.each do |line|
41
+ if line =~ /^ CONSTRAINT [`"](.+?)[`"] FOREIGN KEY \([`"](.+?)[`"]\) REFERENCES [`"](.+?)[`"] \((.+?)\)( ON DELETE (.+?))?( ON UPDATE (.+?))?,?$/
42
+ name = $1
43
+ column_names = $2
44
+ references_table_name = $3
45
+ references_column_names = $4
46
+ on_update = $8
47
+ on_delete = $6
48
+ on_update = on_update ? on_update.downcase.gsub(' ', '_').to_sym : :restrict
49
+ on_delete = on_delete ? on_delete.downcase.gsub(' ', '_').to_sym : :restrict
50
+
51
+ foreign_keys << ForeignKeyDefinition.new(name,
52
+ table_name, column_names.gsub('`', '').split(', '),
53
+ references_table_name, references_column_names.gsub('`', '').split(', '),
54
+ on_update, on_delete)
55
+ end
56
+ end
57
+ end
58
+
59
+ foreign_keys
60
+ end
61
+
62
+ def reverse_foreign_keys(table_name, name = nil)
63
+ results = execute(<<-SQL, name)
64
+ SELECT constraint_name, table_name, column_name, referenced_table_name, referenced_column_name
65
+ FROM information_schema.key_column_usage
66
+ WHERE table_schema = SCHEMA()
67
+ AND referenced_table_schema = table_schema
68
+ ORDER BY constraint_name, ordinal_position;
69
+ SQL
70
+ current_foreign_key = nil
71
+ foreign_keys = []
72
+
73
+ results.each do |row|
74
+ next unless table_name.casecmp(row[3]) == 0
75
+ if current_foreign_key != row[0]
76
+ foreign_keys << ForeignKeyDefinition.new(row[0], row[1], [], row[3], [])
77
+ current_foreign_key = row[0]
78
+ end
79
+
80
+ foreign_keys.last.column_names << row[2]
81
+ foreign_keys.last.references_column_names << row[4]
82
+ end
83
+
84
+ foreign_keys
85
+ end
86
+
87
+ def views(name = nil)
88
+ views = []
89
+ execute("SELECT table_name FROM information_schema.views WHERE table_schema = SCHEMA()", name).each do |row|
90
+ views << row[0]
91
+ end
92
+ views
93
+ end
94
+
95
+ def view_definition(view_name, name = nil)
96
+ result = execute("SELECT view_definition, check_option FROM information_schema.views WHERE table_schema = SCHEMA() AND table_name = #{quote(view_name)}", name)
97
+ return nil unless (result.respond_to?(:num_rows) ? result.num_rows : result.to_a.size) > 0 # mysql vs mysql2
98
+ row = result.respond_to?(:fetch_row) ? result.fetch_row : result.first
99
+ sql = row[0]
100
+ sql.gsub!(%r{#{quote_table_name(current_database)}[.]}, '')
101
+ case row[1]
102
+ when "CASCADED" then sql += " WITH CASCADED CHECK OPTION"
103
+ when "LOCAL" then sql += " WITH LOCAL CHECK OPTION"
104
+ end
105
+ sql
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end