schema_plus 0.1.0.pre1

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