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,23 @@
1
+ ActiveRecord::Schema.define do
2
+ connection.tables.each do |table| drop_table table end
3
+
4
+ create_table :users, :force => true do |t|
5
+ t.string :login, :index => { :unique => true }
6
+ end
7
+
8
+ create_table :members, :force => true do |t|
9
+ t.string :login
10
+ end
11
+
12
+ create_table :comments, :force => true do |t|
13
+ t.string :content
14
+ t.integer :user
15
+ t.integer :user_id
16
+ t.foreign_key :user_id, :users, :id
17
+ end
18
+
19
+ create_table :posts, :force => true do |t|
20
+ t.string :content
21
+ end
22
+
23
+ end
@@ -0,0 +1,21 @@
1
+ ActiveRecord::Schema.define do
2
+ connection.tables.each do |table| drop_table table end
3
+
4
+ create_table :users, :force => true do |t|
5
+ t.string :login
6
+ t.datetime :deleted_at
7
+ end
8
+
9
+ create_table :posts, :force => true do |t|
10
+ t.text :body
11
+ t.integer :user_id
12
+ t.integer :author_id
13
+ end
14
+
15
+ create_table :comments, :force => true do |t|
16
+ t.text :body
17
+ t.integer :post_id
18
+ t.foreign_key :post_id, :posts, :id
19
+ end
20
+
21
+ end
@@ -0,0 +1,167 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'stringio'
3
+
4
+ require 'models/post'
5
+
6
+ describe "Schema dump (core)" do
7
+
8
+ before(:all) do
9
+ load_core_schema
10
+ end
11
+
12
+ let(:dump) do
13
+ stream = StringIO.new
14
+ ActiveRecord::SchemaDumper.ignore_tables = %w[users comments]
15
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
16
+ stream.string
17
+ end
18
+
19
+ it "should include foreign_key definition" do
20
+ with_foreign_key Post, :user_id, :users, :id do
21
+ dump.should match(to_regexp(%q{t.foreign_key ["user_id"], "users", ["id"]}))
22
+ end
23
+ end
24
+
25
+ it "should include foreign_key options" do
26
+ with_foreign_key Post, :user_id, :users, :id, :on_update => :cascade, :on_delete => :set_null do
27
+ dump.should match(to_regexp(%q{t.foreign_key ["user_id"], "users", ["id"], :on_update => :cascade, :on_delete => :set_null}))
28
+ end
29
+ end
30
+
31
+ it "should include index definition" do
32
+ with_index Post, :user_id do
33
+ dump.should match(to_regexp(%q{t.index ["user_id"]}))
34
+ end
35
+ end
36
+
37
+ it "should include index name" do
38
+ with_index Post, :user_id, :name => "posts_user_id_index" do
39
+ dump.should match(to_regexp(%q{t.index ["user_id"], :name => "posts_user_id_index"}))
40
+ end
41
+ end
42
+
43
+ it "should define unique index" do
44
+ with_index Post, :user_id, :name => "posts_user_id_index", :unique => true do
45
+ dump.should match(to_regexp(%q{t.index ["user_id"], :name => "posts_user_id_index", :unique => true}))
46
+ end
47
+ end
48
+
49
+ if SchemaPlusHelpers.postgresql?
50
+
51
+ it "should define case insensitive index" do
52
+ with_index Post, :name => "posts_user_body_index", :expression => "USING btree (LOWER(body))" do
53
+ dump.should match(to_regexp(%q{t.index ["body"], :name => "posts_user_body_index", :case_sensitive => false}))
54
+ end
55
+ end
56
+
57
+ it "should define conditions" do
58
+ with_index Post, :user_id, :name => "posts_user_id_index", :conditions => "user_id IS NOT NULL" do
59
+ dump.should match(to_regexp(%q{t.index ["user_id"], :name => "posts_user_id_index", :conditions => "(user_id IS NOT NULL)"}))
60
+ end
61
+ end
62
+
63
+ it "should define expression" do
64
+ with_index Post, :name => "posts_freaky_index", :expression => "USING hash (least(id, user_id))" do
65
+ dump.should match(to_regexp(%q{t.index :name => "posts_freaky_index", :kind => "hash", :expression => "LEAST(id, user_id)"}))
66
+ end
67
+ end
68
+
69
+ it "should define kind" do
70
+ with_index Post, :name => "posts_body_index", :expression => "USING hash (body)" do
71
+ dump.should match(to_regexp(%q{t.index ["body"], :name => "posts_body_index", :kind => "hash"}))
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ protected
78
+ def to_regexp(string)
79
+ Regexp.new(Regexp.escape(string))
80
+ end
81
+
82
+ def with_foreign_key(model, columns, referenced_table_name, referenced_columns, options = {})
83
+ table_columns = model.columns.reject{|column| column.name == 'id'}
84
+ ActiveRecord::Migration.suppress_messages do
85
+ ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
86
+ table_columns.each do |column|
87
+ t.column column.name, column.type
88
+ end
89
+ t.foreign_key columns, referenced_table_name, referenced_columns, options
90
+ end
91
+ end
92
+ model.reset_column_information
93
+ begin
94
+ yield
95
+ ensure
96
+ ActiveRecord::Migration.suppress_messages do
97
+ ActiveRecord::Migration.create_table model.table_name, :force => true do |t|
98
+ table_columns.each do |column|
99
+ t.column column.name, column.type
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def with_index(model, columns, options = {})
107
+ ActiveRecord::Migration.suppress_messages do
108
+ ActiveRecord::Migration.add_index(model.table_name, columns, options)
109
+ end
110
+ model.reset_column_information
111
+ begin
112
+ yield
113
+ ensure
114
+ ActiveRecord::Migration.suppress_messages do
115
+ ActiveRecord::Migration.remove_index(model.table_name, :name => determine_index_name(model, columns, options))
116
+ end
117
+ end
118
+ end
119
+
120
+ def determine_index_name(model, columns, options)
121
+ name = columns[:name] if columns.is_a?(Hash)
122
+ name ||= options[:name]
123
+ name ||= model.indexes.detect { |index| index.table == model.table_name.to_s && index.columns == Array(columns).collect(&:to_s) }.name
124
+ name
125
+ end
126
+
127
+ def determine_foreign_key_name(model, columns, options)
128
+ name = options[:name]
129
+ name ||= model.foreign_keys.detect { |fk| fk.table_name == model.table_name.to_s && fk.column_names == Array(columns).collect(&:to_s) }.name
130
+ end
131
+
132
+ end
133
+
134
+ describe "Schema dump (auto)" do
135
+
136
+ before(:all) do
137
+ load_auto_schema
138
+ end
139
+
140
+ let(:dump) do
141
+ stream = StringIO.new
142
+ ActiveRecord::SchemaDumper.ignore_tables = []
143
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
144
+ stream.string
145
+ end
146
+
147
+ unless SchemaPlusHelpers.sqlite3?
148
+ it "shouldn't include :index option for index" do
149
+ add_column(:author_id, :integer, :references => :users, :index => true) do
150
+ dump.should_not match(/index => true/)
151
+ end
152
+ end
153
+ end
154
+
155
+ protected
156
+ def add_column(column_name, *args)
157
+ table = Post.table_name
158
+ ActiveRecord::Migration.suppress_messages do
159
+ ActiveRecord::Migration.add_column(table, column_name, *args)
160
+ Post.reset_column_information
161
+ yield if block_given?
162
+ ActiveRecord::Migration.remove_column(table, column_name)
163
+ end
164
+ end
165
+
166
+ end
167
+
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe ActiveRecord::Schema do
4
+
5
+ let(:schema) { ActiveRecord::Schema }
6
+
7
+ let(:connection) { ActiveRecord::Base.connection }
8
+
9
+ context "defining with auto_index and auto_create" do
10
+
11
+ around(:each) do |example|
12
+ with_auto_index do
13
+ with_auto_create do
14
+ example.run
15
+ end
16
+ end
17
+ end
18
+
19
+ it "should pass" do
20
+ expect { define_schema }.should_not raise_error
21
+ end
22
+
23
+ it "should create only explicity added indexes" do
24
+ define_schema
25
+ connection.tables.collect { |table| connection.indexes(table) }.flatten.should have(1).item
26
+ end
27
+
28
+ it "should create only explicity added foriegn keys" do
29
+ define_schema
30
+ connection.tables.collect { |table| connection.foreign_keys(table) }.flatten.should have(1).item
31
+ end
32
+
33
+ end
34
+
35
+ protected
36
+ def define_schema
37
+ ActiveRecord::Migration.suppress_messages do
38
+ schema.define do
39
+ connection.tables.each do |table| drop_table table end
40
+
41
+ create_table :users, :force => true do
42
+ end
43
+
44
+ create_table :posts, :force => true do |t|
45
+ t.integer :user_id, :references => :users, :index => true
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def with_auto_index(value = true)
52
+ old_value = SchemaPlus.config.foreign_keys.auto_index
53
+ SchemaPlus.config.foreign_keys.auto_index = value
54
+ begin
55
+ yield
56
+ ensure
57
+ SchemaPlus.config.foreign_keys.auto_index = old_value
58
+ end
59
+ end
60
+
61
+ def with_auto_create(value = true)
62
+ old_value = SchemaPlus.config.foreign_keys.auto_create
63
+ SchemaPlus.config.foreign_keys.auto_create = value
64
+ begin
65
+ yield
66
+ ensure
67
+ SchemaPlus.config.foreign_keys.auto_create = old_value
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,59 @@
1
+ if RUBY_VERSION > "1.9"
2
+ require 'simplecov'
3
+ require 'simplecov-gem-adapter'
4
+ SimpleCov.start "gem"
5
+ end
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+
10
+ require 'rspec'
11
+ require 'active_record'
12
+ require 'schema_plus'
13
+ require 'connection'
14
+
15
+ SchemaPlus.insert
16
+
17
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
18
+
19
+ RSpec.configure do |config|
20
+ config.include(SchemaPlusMatchers)
21
+ config.include(SchemaPlusHelpers)
22
+ end
23
+
24
+ def load_schema(name)
25
+ ActiveRecord::Migration.suppress_messages do
26
+ eval(File.read(File.join(File.dirname(__FILE__), 'schema', name)))
27
+ end
28
+ end
29
+
30
+ def load_core_schema
31
+ SchemaPlus.setup do |config|
32
+ config.foreign_keys.auto_create = false;
33
+ end
34
+ load_schema('core_schema.rb')
35
+ load 'models/user.rb'
36
+ load 'models/post.rb'
37
+ load 'models/comment.rb'
38
+ end
39
+
40
+ def load_auto_schema
41
+ SchemaPlus.setup do |config|
42
+ config.foreign_keys.auto_create = true;
43
+ end
44
+ load_schema('auto_schema.rb')
45
+ load 'models/user.rb'
46
+ load 'models/post.rb'
47
+ load 'models/comment.rb'
48
+ end
49
+
50
+ def remove_all_models
51
+ ObjectSpace.each_object(Class) do |c|
52
+ next unless c.ancestors.include? ActiveRecord::Base
53
+ next if c == ActiveRecord::Base
54
+ next if c.name.blank?
55
+ ActiveSupport::Dependencies.remove_constant c.name
56
+ end
57
+ end
58
+
59
+ SimpleCov.command_name ActiveRecord::Base.connection.adapter_name if defined? SimpleCov
@@ -0,0 +1,13 @@
1
+ # ported from rspec-rails
2
+ # There is no reason to install whole gem as we
3
+ # need only that tiny helper
4
+ module ::ActiveModel::Validations
5
+
6
+ def error_on(attribute)
7
+ self.valid?
8
+ [self.errors[attribute]].flatten.compact
9
+ end
10
+
11
+ alias :errors_on :error_on
12
+
13
+ end
@@ -0,0 +1,16 @@
1
+ module SchemaPlusHelpers
2
+ extend self
3
+
4
+ def mysql?
5
+ ActiveRecord::Base.connection.adapter_name =~ /^mysql/i
6
+ end
7
+
8
+ def postgresql?
9
+ ActiveRecord::Base.connection.adapter_name =~ /^postgresql/i
10
+ end
11
+
12
+ def sqlite3?
13
+ ActiveRecord::Base.connection.adapter_name =~ /^sqlite/i
14
+ end
15
+
16
+ end
@@ -0,0 +1,2 @@
1
+ require 'support/matchers/reference'
2
+ require 'support/matchers/have_index'
@@ -0,0 +1,52 @@
1
+ module SchemaPlusMatchers
2
+
3
+ class HaveIndex
4
+
5
+ def initialize(expectation, options = {})
6
+ set_required_columns(expectation, options)
7
+ end
8
+
9
+ def matches?(model)
10
+ @model = model
11
+ @model.indexes.any? do |index|
12
+ index.columns.to_set == @required_columns &&
13
+ (@unique ? index.unique : true) &&
14
+ (@name ? index.name == @name.to_s : true)
15
+ end
16
+ end
17
+
18
+ def failure_message_for_should(should_not = false)
19
+ invert = should_not ? "not to" : ""
20
+ "Expected #{@model.table_name} to #{invert} contain index on #{@required_columns.entries.inspect}"
21
+ end
22
+
23
+ def failure_message_for_should_not
24
+ failure_message_for_should(true)
25
+ end
26
+
27
+ def on(expectation)
28
+ set_required_columns(expectation)
29
+ self
30
+ end
31
+
32
+ private
33
+ def set_required_columns(expectation, options = {})
34
+ @required_columns = Array(expectation).collect(&:to_s).to_set
35
+ @unique = options.delete(:unique)
36
+ @name = options.delete(:name)
37
+ end
38
+
39
+ end
40
+
41
+ def have_index(*expectation)
42
+ options = expectation.extract_options!
43
+ HaveIndex.new(expectation, options)
44
+ end
45
+
46
+ def have_unique_index(*expectation)
47
+ options = expectation.extract_options!
48
+ options[:unique] = true
49
+ HaveIndex.new(expectation, options)
50
+ end
51
+
52
+ end
@@ -0,0 +1,66 @@
1
+ module SchemaPlusMatchers
2
+
3
+ class Reference
4
+ def initialize(expected)
5
+ @column_names = nil
6
+ unless expected.empty?
7
+ @references_column_names = Array(expected).collect(&:to_s)
8
+ @references_table_name = @references_column_names.shift
9
+ end
10
+ end
11
+
12
+ def matches?(model)
13
+ @model = model
14
+ if @references_table_name
15
+ @result = @model.foreign_keys.select do |fk|
16
+ fk.references_table_name == @references_table_name &&
17
+ @references_column_names.empty? ? true : fk.references_column_names == @references_column_names
18
+ end
19
+ else
20
+ @result = @model.foreign_keys
21
+ end
22
+ if @column_names
23
+ @result.any? do |fk|
24
+ fk.column_names == @column_names &&
25
+ (@on_update ? fk.on_update == @on_update : true) &&
26
+ (@on_delete ? fk.on_delete == @on_delete : true)
27
+ end
28
+ else
29
+ !@result.empty?
30
+ end
31
+ end
32
+
33
+ def failure_message_for_should(should_not = false)
34
+ target_column_names = @column_names.present? ? "(#{@column_names.join(', ')})" : ""
35
+ destinantion_column_names = @references_table_name ? "#{@references_table_name}(#{@references_column_names.join(', ')})" : "anything"
36
+ invert = should_not ? 'not' : ''
37
+ "Expected #{@model.table_name}#{target_column_names} to #{invert} reference #{destinantion_column_names}"
38
+ end
39
+
40
+ def failure_message_for_should_not
41
+ failure_message_for_should(true)
42
+ end
43
+
44
+ def on(*column_names)
45
+ @column_names = column_names.collect(&:to_s)
46
+ self
47
+ end
48
+
49
+ def on_update(action)
50
+ @on_update = action
51
+ self
52
+ end
53
+
54
+ def on_delete(action)
55
+ @on_delete = action
56
+ self
57
+ end
58
+
59
+ end
60
+
61
+ def reference(*expect)
62
+ Reference.new(expect)
63
+ end
64
+
65
+ end
66
+