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