Empact-activerecord-import 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +25 -0
- data/Rakefile +72 -0
- data/VERSION +1 -0
- data/lib/activerecord-import.rb +16 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +10 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
- data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +6 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +7 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +7 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +125 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +50 -0
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +13 -0
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -0
- data/lib/activerecord-import/base.rb +27 -0
- data/lib/activerecord-import/import.rb +347 -0
- data/lib/activerecord-import/mysql.rb +8 -0
- data/lib/activerecord-import/mysql2.rb +8 -0
- data/lib/activerecord-import/postgresql.rb +8 -0
- data/lib/activerecord-import/sqlite3.rb +8 -0
- data/test/active_record/connection_adapter_test.rb +52 -0
- data/test/adapters/mysql.rb +1 -0
- data/test/adapters/mysql2.rb +1 -0
- data/test/adapters/postgresql.rb +1 -0
- data/test/adapters/sqlite3.rb +1 -0
- data/test/import_test.rb +202 -0
- data/test/models/book.rb +3 -0
- data/test/models/group.rb +3 -0
- data/test/models/topic.rb +7 -0
- data/test/mysql/import_test.rb +6 -0
- data/test/mysql2/import_test.rb +6 -0
- data/test/postgresql/import_test.rb +20 -0
- data/test/schema/generic_schema.rb +98 -0
- data/test/schema/mysql_schema.rb +17 -0
- data/test/schema/version.rb +4 -0
- data/test/support/active_support/test_case_extensions.rb +67 -0
- data/test/support/factories.rb +13 -0
- data/test/support/generate.rb +29 -0
- data/test/support/mysql/assertions.rb +55 -0
- data/test/support/mysql/import_examples.rb +117 -0
- data/test/test_helper.rb +46 -0
- metadata +169 -0
@@ -0,0 +1,8 @@
|
|
1
|
+
warn <<-MSG
|
2
|
+
[DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
|
3
|
+
is deprecated. Update to autorequire using 'require "activerecord-import"'. See
|
4
|
+
http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
|
5
|
+
MSG
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import"))
|
8
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
warn <<-MSG
|
2
|
+
[DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
|
3
|
+
is deprecated. Update to autorequire using 'require "activerecord-import"'. See
|
4
|
+
http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
|
5
|
+
MSG
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import"))
|
8
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
warn <<-MSG
|
2
|
+
[DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
|
3
|
+
is deprecated. Update to autorequire using 'require "activerecord-import"'. See
|
4
|
+
http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
|
5
|
+
MSG
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import"))
|
8
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
warn <<-MSG
|
2
|
+
[DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
|
3
|
+
is deprecated. Update to autorequire using 'require "activerecord-import"'. See
|
4
|
+
http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
|
5
|
+
MSG
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import"))
|
8
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
describe "ActiveRecord::ConnectionAdapter::AbstractAdapter" do
|
4
|
+
context "#get_insert_value_sets - computing insert value sets" do
|
5
|
+
let(:adapter){ ActiveRecord::ConnectionAdapters::AbstractAdapter }
|
6
|
+
let(:base_sql){ "INSERT INTO atable (a,b,c)" }
|
7
|
+
let(:values){ [ "(1,2,3)", "(2,3,4)", "(3,4,5)" ] }
|
8
|
+
|
9
|
+
context "when the max allowed bytes is 33 and the base SQL is 26 bytes" do
|
10
|
+
it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do
|
11
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 33
|
12
|
+
assert_equal 3, value_sets.size
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when the max allowed bytes is 40 and the base SQL is 26 bytes" do
|
17
|
+
it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do
|
18
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 40
|
19
|
+
assert_equal 3, value_sets.size
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when the max allowed bytes is 41 and the base SQL is 26 bytes" do
|
24
|
+
it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do
|
25
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 41
|
26
|
+
assert_equal 2, value_sets.size
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when the max allowed bytes is 48 and the base SQL is 26 bytes" do
|
31
|
+
it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do
|
32
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 48
|
33
|
+
assert_equal 2, value_sets.size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when the max allowed bytes is 49 and the base SQL is 26 bytes" do
|
38
|
+
it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do
|
39
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 49
|
40
|
+
assert_equal 1, value_sets.size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when the max allowed bytes is 999999 and the base SQL is 26 bytes" do
|
45
|
+
it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do
|
46
|
+
value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 999999
|
47
|
+
assert_equal 1, value_sets.size
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["ARE_DB"] = "mysql"
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["ARE_DB"] = "mysql2"
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["ARE_DB"] = "postgresql"
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["ARE_DB"] = "sqlite3"
|
data/test/import_test.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "#import" do
|
4
|
+
it "should return the number of inserts performed" do
|
5
|
+
# see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
|
6
|
+
assert_difference "Topic.count", +10 do
|
7
|
+
result = Topic.import Build(3, :topics)
|
8
|
+
assert result.num_inserts > 0
|
9
|
+
|
10
|
+
result = Topic.import Build(7, :topics)
|
11
|
+
assert result.num_inserts > 0
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with :validation option" do
|
16
|
+
let(:columns) { %w(title author_name) }
|
17
|
+
let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
|
18
|
+
let(:invalid_values) { [[ "The RSpec Book", ""], ["Agile+UX", ""]] }
|
19
|
+
|
20
|
+
context "with validation checks turned off" do
|
21
|
+
it "should import valid data" do
|
22
|
+
assert_difference "Topic.count", +2 do
|
23
|
+
result = Topic.import columns, valid_values, :validate => false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should import invalid data" do
|
28
|
+
assert_difference "Topic.count", +2 do
|
29
|
+
result = Topic.import columns, invalid_values, :validate => false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with validation checks turned on" do
|
35
|
+
it "should import valid data" do
|
36
|
+
assert_difference "Topic.count", +2 do
|
37
|
+
result = Topic.import columns, valid_values, :validate => true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not import invalid data" do
|
42
|
+
assert_no_difference "Topic.count" do
|
43
|
+
result = Topic.import columns, invalid_values, :validate => true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should report the failed instances" do
|
48
|
+
results = Topic.import columns, invalid_values, :validate => true
|
49
|
+
assert_equal invalid_values.size, results.failed_instances.size
|
50
|
+
results.failed_instances.each{ |e| assert_kind_of Topic, e }
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should import valid data when mixed with invalid data" do
|
54
|
+
assert_difference "Topic.count", +2 do
|
55
|
+
result = Topic.import columns, valid_values + invalid_values, :validate => true
|
56
|
+
end
|
57
|
+
assert_equal 0, Topic.find_all_by_title(invalid_values.map(&:first)).count
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with an array of unsaved model instances" do
|
63
|
+
let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")}
|
64
|
+
let(:topics) { Build(9, :topics) }
|
65
|
+
let(:invalid_topics){ Build(7, :invalid_topics)}
|
66
|
+
|
67
|
+
it "should import records based on those model's attributes" do
|
68
|
+
assert_difference "Topic.count", +9 do
|
69
|
+
result = Topic.import topics
|
70
|
+
end
|
71
|
+
|
72
|
+
Topic.import [topic]
|
73
|
+
assert Topic.find_by_title_and_author_name("The RSpec Book", "David Chelimsky")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not overwrite existing records" do
|
77
|
+
topic = Generate(:topic, :title => "foobar")
|
78
|
+
assert_no_difference "Topic.count" do
|
79
|
+
begin
|
80
|
+
Topic.transaction do
|
81
|
+
topic.title = "baz"
|
82
|
+
Topic.import [topic]
|
83
|
+
end
|
84
|
+
rescue Exception
|
85
|
+
# PostgreSQL raises PgError due to key constraints
|
86
|
+
# I don't know why ActiveRecord doesn't catch these. *sigh*
|
87
|
+
end
|
88
|
+
end
|
89
|
+
assert_equal "foobar", topic.reload.title
|
90
|
+
end
|
91
|
+
|
92
|
+
context "with validation checks turned on" do
|
93
|
+
it "should import valid models" do
|
94
|
+
assert_difference "Topic.count", +9 do
|
95
|
+
result = Topic.import topics, :validate => true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not import invalid models" do
|
100
|
+
assert_no_difference "Topic.count" do
|
101
|
+
result = Topic.import invalid_topics, :validate => true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "with validation checks turned off" do
|
107
|
+
it "should import invalid models" do
|
108
|
+
assert_difference "Topic.count", +7 do
|
109
|
+
result = Topic.import invalid_topics, :validate => false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "with an array of columns and an array of unsaved model instances" do
|
116
|
+
let(:topics) { Build(2, :topics) }
|
117
|
+
|
118
|
+
it "should import records populating the supplied columns with the corresponding model instance attributes" do
|
119
|
+
assert_difference "Topic.count", +2 do
|
120
|
+
result = Topic.import [:author_name, :title], topics
|
121
|
+
end
|
122
|
+
|
123
|
+
# imported topics should be findable by their imported attributes
|
124
|
+
assert Topic.find_by_author_name(topics.first.author_name)
|
125
|
+
assert Topic.find_by_author_name(topics.last.author_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should not populate fields for columns not imported" do
|
129
|
+
topics.first.author_email_address = "zach.dennis@gmail.com"
|
130
|
+
assert_difference "Topic.count", +2 do
|
131
|
+
result = Topic.import [:author_name, :title], topics
|
132
|
+
end
|
133
|
+
|
134
|
+
assert !Topic.find_by_author_email_address("zach.dennis@gmail.com")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "ActiveRecord timestamps" do
|
139
|
+
context "when the timestamps columns are present" do
|
140
|
+
setup do
|
141
|
+
Delorean.time_travel_to("5 minutes ago") do
|
142
|
+
assert_difference "Book.count", +1 do
|
143
|
+
result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
@book = Book.last
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should set the created_at column for new records" do
|
150
|
+
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_at.strftime("%H:%M")
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should set the created_on column for new records" do
|
154
|
+
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_on.strftime("%H:%M")
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should set the updated_at column for new records" do
|
158
|
+
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_at.strftime("%H:%M")
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should set the updated_on column for new records" do
|
162
|
+
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_on.strftime("%H:%M")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
context "when a custom time zone is set" do
|
167
|
+
setup do
|
168
|
+
original_timezone = ActiveRecord::Base.default_timezone
|
169
|
+
ActiveRecord::Base.default_timezone = :utc
|
170
|
+
Delorean.time_travel_to("5 minutes ago") do
|
171
|
+
assert_difference "Book.count", +1 do
|
172
|
+
result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
ActiveRecord::Base.default_timezone = original_timezone
|
176
|
+
@book = Book.last
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should set the created_at and created_on timestamps for new records" do
|
180
|
+
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_at.strftime("%H:%M")
|
181
|
+
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_on.strftime("%H:%M")
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should set the updated_at and updated_on timestamps for new records" do
|
185
|
+
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_at.strftime("%H:%M")
|
186
|
+
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_on.strftime("%H:%M")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context "importing with database reserved words" do
|
192
|
+
let(:group) { Build(:group, :order => "superx") }
|
193
|
+
|
194
|
+
it "should import just fine" do
|
195
|
+
assert_difference "Group.count", +1 do
|
196
|
+
result = Group.import [group]
|
197
|
+
end
|
198
|
+
assert_equal "superx", Group.first.order
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
data/test/models/book.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
class Topic < ActiveRecord::Base
|
2
|
+
validates_presence_of :author_name
|
3
|
+
has_many :books
|
4
|
+
belongs_to :parent, :class_name => "Topic"
|
5
|
+
|
6
|
+
composed_of :description, :mapping => [ %w(title title), %w(author_name author_name)], :allow_nil => true, :class_name => "TopicDescription"
|
7
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/assertions')
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
|
5
|
+
|
6
|
+
should_support_mysql_import_functionality
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/assertions')
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
|
5
|
+
|
6
|
+
should_support_mysql_import_functionality
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "#supports_imports?" do
|
4
|
+
it "should support import" do
|
5
|
+
assert ActiveRecord::Base.supports_import?
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#import" do
|
10
|
+
it "should import with a single insert" do
|
11
|
+
# see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
|
12
|
+
assert_difference "Topic.count", +10 do
|
13
|
+
result = Topic.import Build(3, :topics)
|
14
|
+
assert_equal 1, result.num_inserts
|
15
|
+
|
16
|
+
result = Topic.import Build(7, :topics)
|
17
|
+
assert_equal 1, result.num_inserts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
create_table :schema_info, :force=>true do |t|
|
4
|
+
t.column :version, :integer, :unique=>true
|
5
|
+
end
|
6
|
+
SchemaInfo.create :version=>SchemaInfo::VERSION
|
7
|
+
|
8
|
+
create_table :group, :force => true do |t|
|
9
|
+
t.column :order, :string
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :topics, :force=>true do |t|
|
14
|
+
t.column :title, :string, :null => false
|
15
|
+
t.column :author_name, :string
|
16
|
+
t.column :author_email_address, :string
|
17
|
+
t.column :written_on, :datetime
|
18
|
+
t.column :bonus_time, :time
|
19
|
+
t.column :last_read, :datetime
|
20
|
+
t.column :content, :text
|
21
|
+
t.column :approved, :boolean, :default=>'1'
|
22
|
+
t.column :replies_count, :integer
|
23
|
+
t.column :parent_id, :integer
|
24
|
+
t.column :type, :string
|
25
|
+
t.column :created_at, :datetime
|
26
|
+
t.column :created_on, :datetime
|
27
|
+
t.column :updated_at, :datetime
|
28
|
+
t.column :updated_on, :datetime
|
29
|
+
end
|
30
|
+
|
31
|
+
create_table :projects, :force=>true do |t|
|
32
|
+
t.column :name, :string
|
33
|
+
t.column :type, :string
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :developers, :force=>true do |t|
|
37
|
+
t.column :name, :string
|
38
|
+
t.column :salary, :integer, :default=>'70000'
|
39
|
+
t.column :created_at, :datetime
|
40
|
+
t.column :team_id, :integer
|
41
|
+
t.column :updated_at, :datetime
|
42
|
+
end
|
43
|
+
|
44
|
+
create_table :addresses, :force=>true do |t|
|
45
|
+
t.column :address, :string
|
46
|
+
t.column :city, :string
|
47
|
+
t.column :state, :string
|
48
|
+
t.column :zip, :string
|
49
|
+
t.column :developer_id, :integer
|
50
|
+
end
|
51
|
+
|
52
|
+
create_table :teams, :force=>true do |t|
|
53
|
+
t.column :name, :string
|
54
|
+
end
|
55
|
+
|
56
|
+
create_table :books, :force=>true do |t|
|
57
|
+
t.column :title, :string, :null=>false
|
58
|
+
t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
|
59
|
+
t.column :author_name, :string, :null=>false
|
60
|
+
t.column :created_at, :datetime
|
61
|
+
t.column :created_on, :datetime
|
62
|
+
t.column :updated_at, :datetime
|
63
|
+
t.column :updated_on, :datetime
|
64
|
+
t.column :publish_date, :date
|
65
|
+
t.column :topic_id, :integer
|
66
|
+
t.column :for_sale, :boolean, :default => true
|
67
|
+
end
|
68
|
+
|
69
|
+
create_table :languages, :force=>true do |t|
|
70
|
+
t.column :name, :string
|
71
|
+
t.column :developer_id, :integer
|
72
|
+
end
|
73
|
+
|
74
|
+
create_table :shopping_carts, :force=>true do |t|
|
75
|
+
t.column :name, :string, :null => true
|
76
|
+
t.column :created_at, :datetime
|
77
|
+
t.column :updated_at, :datetime
|
78
|
+
end
|
79
|
+
|
80
|
+
create_table :cart_items, :force => true do |t|
|
81
|
+
t.column :shopping_cart_id, :string, :null => false
|
82
|
+
t.column :book_id, :string, :null => false
|
83
|
+
t.column :copies, :integer, :default => 1
|
84
|
+
t.column :created_at, :datetime
|
85
|
+
t.column :updated_at, :datetime
|
86
|
+
end
|
87
|
+
|
88
|
+
add_index :cart_items, [:shopping_cart_id, :book_id], :unique => true, :name => 'uk_shopping_cart_books'
|
89
|
+
|
90
|
+
create_table :animals, :force => true do |t|
|
91
|
+
t.column :name, :string, :null => false
|
92
|
+
t.column :size, :string, :default => nil
|
93
|
+
t.column :created_at, :datetime
|
94
|
+
t.column :updated_at, :datetime
|
95
|
+
end
|
96
|
+
|
97
|
+
add_index :animals, [:name], :unique => true, :name => 'uk_animals'
|
98
|
+
end
|