hobofields 0.8.10 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -2,6 +2,9 @@ require 'rubygems'
2
2
  require 'activerecord'
3
3
  ActiveRecord::ActiveRecordError # hack for https://rails.lighthouseapp.com/projects/8994/tickets/2577-when-using-activerecordassociations-outside-of-rails-a-nameerror-is-thrown
4
4
 
5
+ RUBY = ENV['RUBY'] || (defined?(JRUBY_VERSION) ? 'jruby' : 'ruby')
6
+ RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} `which rubydoctest`"
7
+
5
8
  $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobofields/lib')
6
9
  $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobosupport/lib')
7
10
  require 'hobosupport'
@@ -10,13 +13,13 @@ require 'hobofields'
10
13
  namespace "test" do
11
14
  desc "Run the doctests"
12
15
  task :doctest do |t|
13
- exit(1) if !system("rubydoctest test/*.rdoctest")
16
+ exit(1) if !system("#{RUBYDOCTEST} test/*.rdoctest")
14
17
  end
15
18
 
16
19
  desc "Run the unit tests"
17
20
  task :unit do |t|
18
21
  Dir["test/test_*.rb"].each do |f|
19
- exit(1) if !system("ruby #{f}")
22
+ exit(1) if !system("#{RUBY} #{f}")
20
23
  end
21
24
  end
22
25
  end
@@ -30,7 +33,7 @@ begin
30
33
  gemspec.summary = "Rich field types and migration generator for Rails"
31
34
  gemspec.homepage = "http://hobocentral.net/"
32
35
  gemspec.authors = ["Tom Locke"]
33
- gemspec.rubyforge_project = "hobofields"
36
+ gemspec.rubyforge_project = "hobo"
34
37
  gemspec.add_dependency("rails", [">= 2.2.2"])
35
38
  gemspec.add_dependency("hobosupport", ["= #{HoboFields::VERSION}"])
36
39
  end
@@ -5,7 +5,7 @@ module HoboFields
5
5
  COLUMN_TYPE = :string
6
6
 
7
7
  def validate
8
- "is not valid" unless valid? || blank?
8
+ I18n.t("activerecord.errors.messages.invalid") unless valid? || blank?
9
9
  end
10
10
 
11
11
  def valid?
@@ -28,6 +28,7 @@ module HoboFields
28
28
  c = Class.new(EnumString) do
29
29
  values.each do |v|
30
30
  const_name = v.upcase.gsub(/[^a-z0-9_]/i, '_').gsub(/_+/, '_')
31
+ const_name = "V" + const_name if const_name =~ /^[0-9_]/
31
32
  const_set(const_name, self.new(v)) unless const_defined?(const_name)
32
33
 
33
34
  method_name = "is_#{v.underscore}?"
@@ -17,6 +17,18 @@ module HoboFields
17
17
 
18
18
  TYPE_SYNONYMS = [[:timestamp, :datetime]]
19
19
 
20
+ begin
21
+ MYSQL_COLUMN_CLASS = ActiveRecord::ConnectionAdapters::MysqlColumn
22
+ rescue NameError
23
+ MYSQL_COLUMN_CLASS = NilClass
24
+ end
25
+
26
+ begin
27
+ SQLITE_COLUMN_CLASS = ActiveRecord::ConnectionAdapters::SQLiteColumn
28
+ rescue NameError
29
+ SQLITE_COLUMN_CLASS = NilClass
30
+ end
31
+
20
32
  def sql_type
21
33
  options[:sql_type] or begin
22
34
  if native_type?(type)
@@ -55,7 +67,7 @@ module HoboFields
55
67
  return col_spec.type.in?(synonyms)
56
68
  end
57
69
  end
58
- t = col_spec.type
70
+ t == col_spec.type
59
71
  end
60
72
 
61
73
 
@@ -63,9 +75,16 @@ module HoboFields
63
75
  !same_type?(col_spec) ||
64
76
  begin
65
77
  check_attributes = [:null, :default]
66
- check_attributes += [:precision, :scale] if sql_type == :decimal
78
+ check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
79
+ check_attributes -= [:default] if sql_type == :text && col_spec.is_a?(MYSQL_COLUMN_CLASS)
67
80
  check_attributes << :limit if sql_type.in?([:string, :text, :binary, :integer])
68
- check_attributes.any? { |k| col_spec.send(k) != self.send(k) }
81
+ check_attributes.any? do |k|
82
+ if k==:default && sql_type==:datetime
83
+ col_spec.default.try.to_datetime != default.try.to_datetime
84
+ else
85
+ col_spec.send(k) != self.send(k)
86
+ end
87
+ end
69
88
  end
70
89
  end
71
90
 
@@ -0,0 +1,45 @@
1
+ module HoboFields
2
+
3
+ class IndexSpec
4
+
5
+ def initialize(model, fields, options={})
6
+ @model = model
7
+ self.table = options.delete(:table_name) || model.table_name
8
+ self.fields = Array.wrap(fields).*.to_s
9
+ self.name = options.delete(:name) || model.connection.index_name(self.table, :column => self.fields)
10
+ self.unique = options.delete(:unique) || false
11
+ end
12
+
13
+ attr_accessor :table, :fields, :name, :unique
14
+
15
+ # extract IndexSpecs from an existing table
16
+ def self.for_model(model, old_table_name=nil)
17
+ t = old_table_name || model.table_name
18
+ model.connection.indexes(t).map do |i|
19
+ self.new(model, i.columns, :name => i.name, :unique => i.unique, :table_name => old_table_name)
20
+ end
21
+ end
22
+
23
+ def default_name?
24
+ name == @model.connection.index_name(table, :column => fields)
25
+ end
26
+
27
+ def to_add_statement(new_table_name)
28
+ r = "add_index :#{new_table_name}, #{fields.*.to_sym.inspect}"
29
+ r += ", :unique => true" if unique
30
+ r += ", :name => '#{name}'" unless default_name?
31
+ r
32
+ end
33
+
34
+ def hash
35
+ [table, fields, name, unique].hash
36
+ end
37
+
38
+ def ==(v)
39
+ v.hash == hash
40
+ end
41
+ alias_method :eql?, :==
42
+
43
+ end
44
+
45
+ end
@@ -8,7 +8,7 @@ module HoboFields
8
8
  @ignore_tables = []
9
9
 
10
10
  class << self
11
- attr_accessor :ignore_models, :ignore_tables
11
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing
12
12
  end
13
13
 
14
14
  def self.run(renames={})
@@ -63,13 +63,22 @@ module HoboFields
63
63
  end
64
64
  def native_types; self.class.native_types; end
65
65
 
66
+ # list habtm join tables
67
+ def habtm_tables
68
+ ActiveRecord::Base.send(:subclasses).map do |c|
69
+ c.reflect_on_all_associations(:has_and_belongs_to_many).map { |a| a.options[:join_table] }
70
+ end.flatten.compact.*.to_s
71
+ end
72
+
66
73
  # Returns an array of model classes and an array of table names
67
74
  # that generation needs to take into account
68
75
  def models_and_tables
69
76
  ignore_model_names = MigrationGenerator.ignore_models.map &it.to_s.underscore
70
- models = table_model_classes.select { |m| m < HoboFields::ModelExtensions && m.name.underscore.not_in?(ignore_model_names) }
71
- db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s
72
- [models, db_tables]
77
+ all_models = table_model_classes
78
+ hobo_models = all_models.select { |m| m < HoboFields::ModelExtensions && m.name.underscore.not_in?(ignore_model_names) }
79
+ non_hobo_models = all_models - hobo_models
80
+ db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s - non_hobo_models.*.table_name - habtm_tables
81
+ [hobo_models, db_tables]
73
82
  end
74
83
 
75
84
 
@@ -128,6 +137,7 @@ module HoboFields
128
137
 
129
138
 
130
139
  def always_ignore_tables
140
+ # TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
131
141
  sessions_table = CGI::Session::ActiveRecordStore::Session.table_name if
132
142
  defined?(CGI::Session::ActiveRecordStore::Session) &&
133
143
  defined?(ActionController::Base) &&
@@ -200,7 +210,11 @@ module HoboFields
200
210
  primary_key_option = ", :primary_key => :#{model.primary_key}" if model.primary_key != "id"
201
211
  (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
202
212
  model.field_specs.values.sort_by{|f| f.position}.map {|f| create_field(f, longest_field_name)} +
203
- ["end"]) * "\n"
213
+ ["end"] + (MigrationGenerator.disable_indexing ? [] : create_indexes(model))) * "\n"
214
+ end
215
+
216
+ def create_indexes(model)
217
+ model.index_specs.map { |i| i.to_add_statement(model.table_name) }
204
218
  end
205
219
 
206
220
  def create_field(field_spec, field_name_width)
@@ -276,10 +290,36 @@ module HoboFields
276
290
  end
277
291
  end.compact
278
292
 
279
- [(renames + adds + removes + changes) * "\n",
280
- (undo_renames + undo_adds + undo_removes + undo_changes) * "\n"]
293
+ index_changes, undo_index_changes = change_indexes(model, current_table_name)
294
+
295
+ [(renames + adds + removes + changes + index_changes) * "\n",
296
+ (undo_renames + undo_adds + undo_removes + undo_changes + undo_index_changes) * "\n"]
281
297
  end
282
298
 
299
+ def change_indexes(model, old_table_name)
300
+ return [[],[]] if MigrationGenerator.disable_indexing
301
+ new_table_name = model.table_name
302
+ existing_indexes = IndexSpec.for_model(model, old_table_name)
303
+ model_indexes = model.index_specs
304
+ add_indexes = model_indexes - existing_indexes
305
+ drop_indexes = existing_indexes - model_indexes
306
+ undo_add_indexes = []
307
+ undo_drop_indexes = []
308
+ add_indexes.map! do |i|
309
+ undo_add_indexes << drop_index(old_table_name, i.name)
310
+ i.to_add_statement(new_table_name)
311
+ end
312
+ drop_indexes.map! do |i|
313
+ undo_drop_indexes << i.to_add_statement(old_table_name)
314
+ drop_index(new_table_name, i.name)
315
+ end
316
+ # the order is important here - adding a :unique, for instance needs to remove then add
317
+ [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
318
+ end
319
+
320
+ def drop_index(table, name)
321
+ "remove_index :#{table}, :name => :#{name}"
322
+ end
283
323
 
284
324
  def format_options(options, type, changing=false)
285
325
  options.map do |k, v|
@@ -15,16 +15,19 @@ module HoboFields
15
15
  # and speeds things up a little.
16
16
  inheriting_cattr_reader :field_specs => HashWithIndifferentAccess.new
17
17
 
18
+ # index_specs holds IndexSpec objects for all the declared indexes.
19
+ inheriting_cattr_reader :index_specs => []
20
+
18
21
  def self.inherited(klass)
19
22
  fields do |f|
20
23
  f.field(inheritance_column, :string)
21
24
  end
25
+ index(inheritance_column) unless index_specs.*.fields.include?([inheritance_column])
22
26
  super
23
27
  end
24
28
 
25
-
26
- def self.field_specs
27
- @field_specs ||= HashWithIndifferentAccess.new
29
+ def self.index(fields, options = {})
30
+ index_specs << IndexSpec.new(self, fields, options)
28
31
  end
29
32
 
30
33
 
@@ -52,8 +55,9 @@ module HoboFields
52
55
  type = HoboFields.to_class(type)
53
56
  attrs.each do |attr|
54
57
  declare_attr_type attr, type, options
58
+ type_wrapper = attr_type(attr)
55
59
  define_method "#{attr}=" do |val|
56
- if !val.is_a?(type) && HoboFields.can_wrap?(type, val)
60
+ if type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values) && !val.is_a?(type) && HoboFields.can_wrap?(type, val)
57
61
  val = type.new(val.to_s)
58
62
  end
59
63
  instance_variable_set("@#{attr}", val)
@@ -68,11 +72,19 @@ module HoboFields
68
72
  column_options = {}
69
73
  column_options[:null] = options.delete(:null) if options.has_key?(:null)
70
74
 
75
+ index_options = {}
76
+ index_options[:name] = options.delete(:index) if options.has_key?(:index)
77
+
71
78
  returning belongs_to_without_field_declarations(name, options, &block) do
72
79
  refl = reflections[name.to_sym]
73
80
  fkey = refl.primary_key_name
74
81
  declare_field(fkey.to_sym, :integer, column_options)
75
- declare_polymorphic_type_field(name, column_options) if refl.options[:polymorphic]
82
+ if refl.options[:polymorphic]
83
+ declare_polymorphic_type_field(name, column_options)
84
+ index(["#{name}_type", fkey], index_options) if index_options[:name]!=false
85
+ else
86
+ index(fkey, index_options) if index_options[:name]!=false
87
+ end
76
88
  end
77
89
  end
78
90
  class << self
@@ -110,6 +122,7 @@ module HoboFields
110
122
  try.field_added(name, type, args, options)
111
123
  add_formatting_for_field(name, type, args)
112
124
  add_validations_for_field(name, type, args)
125
+ add_index_for_field(name, args, options)
113
126
  declare_attr_type(name, type, options) unless HoboFields.plain_type?(type)
114
127
  field_specs[name] = FieldSpec.new(self, name, type, options)
115
128
  attr_order << name unless name.in?(attr_order)
@@ -120,7 +133,7 @@ module HoboFields
120
133
  # field declaration
121
134
  def self.add_validations_for_field(name, type, args)
122
135
  validates_presence_of name if :required.in?(args)
123
- validates_uniqueness_of name if :unique.in?(args)
136
+ validates_uniqueness_of name, :allow_nil => !:required.in?(args) if :unique.in?(args)
124
137
 
125
138
  type_class = HoboFields.to_class(type)
126
139
  if type_class && "validate".in?(type_class.public_instance_methods)
@@ -131,7 +144,6 @@ module HoboFields
131
144
  end
132
145
  end
133
146
 
134
-
135
147
  def self.add_formatting_for_field(name, type, args)
136
148
  type_class = HoboFields.to_class(type)
137
149
  if type_class && "format".in?(type_class.instance_methods)
@@ -141,6 +153,16 @@ module HoboFields
141
153
  end
142
154
  end
143
155
 
156
+ def self.add_index_for_field(name, args, options)
157
+ to_name = options.delete(:index)
158
+ return unless to_name
159
+ index_opts = {}
160
+ index_opts[:unique] = :unique.in?(args) || options.delete(:unique)
161
+ # support :index => true declaration
162
+ index_opts[:name] = to_name unless to_name == true
163
+ index(name, index_opts)
164
+ end
165
+
144
166
 
145
167
  # Extended version of the acts_as_list declaration that
146
168
  # automatically delcares the 'position' field
data/lib/hobo_fields.rb CHANGED
@@ -9,7 +9,7 @@ end
9
9
 
10
10
  module HoboFields
11
11
 
12
- VERSION = "0.8.10"
12
+ VERSION = "0.9.0"
13
13
 
14
14
  extend self
15
15
 
@@ -32,7 +32,9 @@ module HoboFields
32
32
  # Provide a lookup for these rather than loading them all preemptively
33
33
 
34
34
  STANDARD_TYPES = {
35
+ :raw_html => "RawHtmlString",
35
36
  :html => "HtmlString",
37
+ :raw_markdown => "RawMarkdownString",
36
38
  :markdown => "MarkdownString",
37
39
  :textile => "TextileString",
38
40
  :password => "PasswordString",
@@ -0,0 +1,47 @@
1
+ Description:
2
+
3
+ This generator compares your existing schema against the
4
+ schema declared inside your fields declarations in your
5
+ models.
6
+
7
+ If the generator finds differences, it will display the
8
+ migration it has created, and ask you if you wish to
9
+ [g]enerate migration, generate and [m]igrate now or [c]ancel?
10
+ Enter "g" to just generate the migration but do not run it.
11
+ Enter "m" to generate the migration and run it, or press "c"
12
+ to do nothing.
13
+
14
+ The generator will then prompt you for the generator name,
15
+ supplying a numbered default name.
16
+
17
+ The generator is conservative and will prompt you to resolve
18
+ any ambiguities.
19
+
20
+ Examples:
21
+
22
+ $ ./script/generate hobo_migration
23
+
24
+ ---------- Up Migration ----------
25
+ create_table :foos do |t|
26
+ t.datetime :created_at
27
+ t.datetime :updated_at
28
+ end
29
+ ----------------------------------
30
+
31
+ ---------- Down Migration --------
32
+ drop_table :foos
33
+ ----------------------------------
34
+ What now: [g]enerate migration, generate and [m]igrate now or [c]ancel? m
35
+
36
+ Migration filename:
37
+ (you can type spaces instead of '_' -- every little helps)
38
+ Filename [hobo_migration_2]: create_foo
39
+ exists db/migrate
40
+ create db/migrate/20091023183838_create_foo.rb
41
+ (in /work/foo)
42
+ == CreateFoo: migrating ======================================================
43
+ -- create_table(:yos)
44
+ -> 0.0856s
45
+ == CreateFoo: migrated (0.0858s) =============================================
46
+
47
+
@@ -133,8 +133,8 @@ class HoboMigrationGenerator < Rails::Generator::Base
133
133
 
134
134
  opt.on("--force-drop", "Don't prompt with 'drop or rename' - just drop everything") { |v| options[:force_drop] = true }
135
135
  opt.on("--default-name", "Dont' prompt for a migration name - just pick one") { |v| options[:default_name] = true }
136
- opt.on("--generate", "Dont' prompt for action - generate the migration") { |v| options[:action] = 'g' }
137
- opt.on("--migrate", "Dont' prompt for action - generate and migrate") { |v| options[:action] = 'm' }
136
+ opt.on("--generate", "Don't prompt for action - generate the migration") { |v| options[:action] = 'g' }
137
+ opt.on("--migrate", "Don't prompt for action - generate and migrate") { |v| options[:action] = 'm' }
138
138
  end
139
139
 
140
140
 
@@ -33,6 +33,6 @@ class HobofieldModelGenerator < Rails::Generator::NamedBase
33
33
  opt.on("--skip-timestamps",
34
34
  "Don't add timestamps to the migration file for this model") { |v| options[:skip_timestamps] = v }
35
35
  opt.on("--skip-fixture",
36
- "Don't generation a fixture file for this model") { |v| options[:skip_fixture] = v}
36
+ "Don't generate a fixture file for this model") { |v| options[:skip_fixture] = v}
37
37
  end
38
38
  end
@@ -30,7 +30,7 @@ Now edit your model as follows:
30
30
 
31
31
  Then, simply run
32
32
 
33
- $ ./script/genearte hobo_migration
33
+ $ ./script/generate hobo_migration
34
34
 
35
35
  And voila
36
36
 
@@ -16,12 +16,17 @@ Our test requires rails:
16
16
  We need a database connection for this test:
17
17
  {.hidden}
18
18
 
19
+ >> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
20
+ >> mysql_user = 'root'; mysql_password = ''
21
+ >> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
19
22
  >> mysql_database = "hobofields_doctest"
20
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
21
- >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
22
- >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
23
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
24
+ >> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
25
+ >> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
23
26
  :database => mysql_database,
24
- :host => "localhost")
27
+ :host => "localhost",
28
+ :username => mysql_user,
29
+ :password => mysql_password)
25
30
  {.hidden}
26
31
 
27
32
  Some load path manipulation you shouldn't need:
@@ -225,14 +230,14 @@ Rich types can define there own validations by a `#validate` method. It should r
225
230
  >> a.contact_address.class
226
231
  => HoboFields::EmailAddress
227
232
  >> a.contact_address.validate
228
- => "is not valid"
233
+ => "is invalid"
229
234
 
230
235
  But normally that method would be called for us during validation:
231
236
 
232
237
  >> a.valid?
233
238
  => false
234
239
  >> a.errors.full_messages
235
- => ["Contact address is not valid"]
240
+ => ["Contact address is invalid"]
236
241
  >> a.contact_address = "me@me.com"
237
242
  >> a.valid?
238
243
  => true
@@ -266,5 +271,5 @@ To have them validated use `validate_virtual_field`:
266
271
  Cleanup
267
272
  {.hidden}
268
273
 
269
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
274
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
270
275
  {.hidden}
@@ -14,12 +14,17 @@ Firstly, in order to test the migration generator outside of a full Rails stack,
14
14
  We also need to get ActiveRecord set up with a database connection
15
15
  {.hidden}
16
16
 
17
+ >> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
18
+ >> mysql_user = 'root'; mysql_password = ''
19
+ >> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
17
20
  >> mysql_database = "hobofields_doctest"
18
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
19
- >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
20
- >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
21
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
22
+ >> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
23
+ >> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
21
24
  :database => mysql_database,
22
- :host => "localhost")
25
+ :host => "localhost",
26
+ :username => mysql_user,
27
+ :password => mysql_password)
23
28
  {.hidden}
24
29
 
25
30
  Some load path manipulation you shouldn't need:
@@ -242,13 +247,16 @@ Note that limit on a decimal column is ignored (use :scale and :precision)
242
247
  => "add_column :adverts, :price, :decimal"
243
248
 
244
249
  Cleanup
250
+ {.hidden}
245
251
 
246
252
  >> Advert.field_specs.delete :price
253
+ {.hidden}
247
254
 
248
255
 
249
256
  ### Foreign Keys
250
257
 
251
- HoboFields extends the `belongs_to` macro so that it also declares the foreign-key field.
258
+ HoboFields extends the `belongs_to` macro so that it also declares the
259
+ foreign-key field. It also generates an index on the field.
252
260
 
253
261
  >>
254
262
  class Advert
@@ -256,13 +264,20 @@ HoboFields extends the `belongs_to` macro so that it also declares the foreign-k
256
264
  end
257
265
  >> up, down = HoboFields::MigrationGenerator.run
258
266
  >> up
259
- => 'add_column :adverts, :category_id, :integer'
267
+ =>
268
+ "add_column :adverts, :category_id, :integer
269
+ add_index :adverts, [:category_id]"
260
270
  >> down
261
- => 'remove_column :adverts, :category_id'
271
+ =>
272
+ "remove_column :adverts, :category_id
273
+ remove_index :adverts, :name => :index_adverts_on_category_id"
262
274
 
263
275
  Cleanup:
276
+ {.hidden}
264
277
 
265
278
  >> Advert.field_specs.delete(:category_id)
279
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
280
+ {.hidden}
266
281
 
267
282
  If you specify a custom foreign key, the migration generator observes that:
268
283
 
@@ -272,14 +287,52 @@ If you specify a custom foreign key, the migration generator observes that:
272
287
  end
273
288
  >> up, down = HoboFields::MigrationGenerator.run
274
289
  >> up
275
- => 'add_column :adverts, :c_id, :integer'
276
- >> down
277
- => 'remove_column :adverts, :c_id'
290
+ =>
291
+ "add_column :adverts, :c_id, :integer
292
+ add_index :adverts, [:c_id]"
278
293
 
279
294
  Cleanup:
295
+ {.hidden}
280
296
 
281
297
  >> Advert.field_specs.delete(:c_id)
298
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["c_id"]}
299
+ {.hidden}
300
+
301
+ You can avoid generating the index by specifying `:index => false`
302
+
303
+ >>
304
+ class Advert
305
+ belongs_to :category, :index => false
306
+ end
307
+ >> up, down = HoboFields::MigrationGenerator.run
308
+ >> up
309
+ => "add_column :adverts, :category_id, :integer"
310
+
311
+ Cleanup:
312
+ {.hidden}
313
+
314
+ >> Advert.field_specs.delete(:category_id)
315
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
316
+ {.hidden}
282
317
 
318
+ You can specify the index name with :index
319
+
320
+ >>
321
+ class Advert
322
+ belongs_to :category, :index => 'my_index'
323
+ end
324
+ >> up, down = HoboFields::MigrationGenerator.run
325
+ >> up
326
+ =>
327
+ "add_column :adverts, :category_id, :integer
328
+ add_index :adverts, [:category_id], :name => 'my_index'"
329
+
330
+ Cleanup:
331
+ {.hidden}
332
+
333
+ >> Advert.field_specs.delete(:category_id)
334
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
335
+ {.hidden}
283
336
 
284
337
  ### Timestamps
285
338
 
@@ -303,10 +356,115 @@ Cleanup:
303
356
  >>
304
357
 
305
358
  Cleanup:
359
+ {.hidden}
306
360
 
307
361
  >> Advert.field_specs.delete(:updated_at)
308
362
  >> Advert.field_specs.delete(:created_at)
363
+ {.hidden}
364
+
365
+ ### Indices
366
+
367
+ You can add an index to a field definition
368
+
369
+ >>
370
+ class Advert
371
+ fields do
372
+ title :string, :index => true
373
+ end
374
+ end
375
+ >> up, down = HoboFields::MigrationGenerator.run
376
+ >> up.split("\n")[1]
377
+ => 'add_index :adverts, [:title]'
378
+
379
+ Cleanup:
380
+ {.hidden}
381
+
382
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
383
+ {.hidden}
384
+
385
+ You can ask for a unique index
386
+
387
+ >>
388
+ class Advert
389
+ fields do
390
+ title :string, :index => true, :unique => true
391
+ end
392
+ end
393
+ >> up, down = HoboFields::MigrationGenerator.run
394
+ >> up.split("\n")[1]
395
+ => 'add_index :adverts, [:title], :unique => true'
396
+
397
+ Cleanup:
398
+ {.hidden}
399
+
400
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
401
+ {.hidden}
402
+
403
+ You can specify the name for the index
404
+
405
+ >>
406
+ class Advert
407
+ fields do
408
+ title :string, :index => 'my_index'
409
+ end
410
+ end
411
+ >> up, down = HoboFields::MigrationGenerator.run
412
+ >> up.split("\n")[1]
413
+ => "add_index :adverts, [:title], :name => 'my_index'"
414
+
415
+ Cleanup:
416
+ {.hidden}
417
+
418
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
419
+ {.hidden}
420
+
421
+ You can ask for an index outside of the fields block
422
+
423
+ >>
424
+ class Advert
425
+ index :title
426
+ end
427
+ >> up, down = HoboFields::MigrationGenerator.run
428
+ >> up.split("\n")[1]
429
+ => "add_index :adverts, [:title]"
430
+
431
+ Cleanup:
432
+ {.hidden}
433
+
434
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
435
+ {.hidden}
436
+
437
+ The available options for the index function are `:unique` and `:name`
438
+
439
+ >>
440
+ class Advert
441
+ index :title, :unique => true, :name => 'my_index'
442
+ end
443
+ >> up, down = HoboFields::MigrationGenerator.run
444
+ >> up.split("\n")[1]
445
+ => "add_index :adverts, [:title], :unique => true, :name => 'my_index'"
446
+
447
+ Cleanup:
448
+ {.hidden}
449
+
450
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
451
+ {.hidden}
452
+
453
+ You can create an index on more than one field
454
+
455
+ >>
456
+ class Advert
457
+ index [:title, :category_id]
458
+ end
459
+ >> up, down = HoboFields::MigrationGenerator.run
460
+ >> up.split("\n")[1]
461
+ => "add_index :adverts, [:title, :category_id]"
462
+
463
+ Cleanup:
464
+ {.hidden}
309
465
 
466
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
467
+ {.hidden}
310
468
 
311
469
  ### Rename a table
312
470
 
@@ -332,27 +490,18 @@ Set the table name back to what it should be and confirm we're in sync:
332
490
  >> HoboFields::MigrationGenerator.run
333
491
  => ["", ""]
334
492
 
335
- ### Drop a table
336
-
337
- If you delete a model, the migration generator will create a `drop_table` migration. Unfortunately there's no way to fully remove the Advert class we've defined from the doctest session, but we can tell the migration generator to ignore it.
338
-
339
- >> HoboFields::MigrationGenerator.ignore_models = [ :advert ]
340
-
341
- Dropping tables is where the automatic down-migration really comes in handy:
342
-
343
- >> up, down = HoboFields::MigrationGenerator.run
344
- >> up
345
- => "drop_table :adverts"
346
- >> down
347
- =>
348
- "create_table "adverts", :force => true do |t|
349
- t.text "body"
350
- t.string "title", :default => "Untitled"
351
- end"
352
-
353
493
  ### Rename a table
354
494
 
355
- As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement'. Remember 'Advert' is being ignored so it's as if we renamed the definition in our models directory.
495
+ As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement', and tell ActiveRecord to forget about the Advert class. This requires code that shouldn't be shown to impressionable children.
496
+ {.hidden}
497
+
498
+ >>
499
+ def nuke_model_class(klass)
500
+ ActiveRecord::Base.instance_eval { class_variable_get('@@subclasses')[klass.superclass].delete(klass) }
501
+ Object.instance_eval { remove_const klass.name.to_sym }
502
+ end
503
+ >> nuke_model_class(Advert)
504
+ {.hidden}
356
505
 
357
506
  >>
358
507
  class Advertisement < ActiveRecord::Base
@@ -367,9 +516,24 @@ As with renaming columns, we have to tell the migration generator about the rena
367
516
  >> down
368
517
  => "rename_table :advertisements, :adverts"
369
518
 
370
- Now that we've seen the renaming we'll switch the 'ignore' setting to ignore that 'Advertisements' class.
519
+ ### Drop a table
520
+
521
+ >> nuke_model_class(Advertisement)
522
+ {.hidden}
523
+
524
+ If you delete a model, the migration generator will create a `drop_table` migration.
371
525
 
372
- >> HoboFields::MigrationGenerator.ignore_models = [ :advertisement ]
526
+ Dropping tables is where the automatic down-migration really comes in handy:
527
+
528
+ >> up, down = HoboFields::MigrationGenerator.run
529
+ >> up
530
+ => "drop_table :adverts"
531
+ >> down
532
+ =>
533
+ "create_table "adverts", :force => true do |t|
534
+ t.text "body"
535
+ t.string "title", :default => "Untitled"
536
+ end"
373
537
 
374
538
  ## STI
375
539
 
@@ -378,19 +542,34 @@ Now that we've seen the renaming we'll switch the 'ignore' setting to ignore tha
378
542
  Adding a subclass or two should introduce the 'type' column and no other changes
379
543
 
380
544
  >>
545
+ class Advert < ActiveRecord::Base
546
+ fields do
547
+ body :text
548
+ title :string, :default => "Untitled"
549
+ end
550
+ end
381
551
  class FancyAdvert < Advert
382
552
  end
383
553
  class SuperFancyAdvert < FancyAdvert
384
554
  end
385
555
  >> up, down = HoboFields::MigrationGenerator.run
386
556
  >> up
387
- => "add_column :adverts, :type, :string"
557
+ =>
558
+ "add_column :adverts, :type, :string
559
+ add_index :adverts, [:type]"
388
560
  >> down
389
- => "remove_column :adverts, :type"
561
+ =>
562
+ "remove_column :adverts, :type
563
+ remove_index :adverts, :name => :index_adverts_on_type"
390
564
 
391
565
  Cleanup
566
+ {.hidden}
392
567
 
393
568
  >> Advert.field_specs.delete(:type)
569
+ >> nuke_model_class(SuperFancyAdvert)
570
+ >> nuke_model_class(FancyAdvert)
571
+ >> Advert.index_specs.delete_if {|spec| spec.fields==["type"]}
572
+ {.hidden}
394
573
 
395
574
 
396
575
  ## Coping with multiple changes
@@ -430,7 +609,9 @@ First let's confirm we're in a known state. One model, 'Advert', with a string '
430
609
 
431
610
  ### Rename a table and add a column
432
611
 
433
- >> HoboFields::MigrationGenerator.ignore_models += [:advert, :fancy_advert, :super_fancy_advert]
612
+ >> nuke_model_class(Advert)
613
+ {.hidden}
614
+
434
615
  >>
435
616
  class Ad < ActiveRecord::Base
436
617
  fields do
@@ -445,7 +626,15 @@ First let's confirm we're in a known state. One model, 'Advert', with a string '
445
626
  "rename_table :adverts, :ads
446
627
 
447
628
  add_column :ads, :created_at, :datetime"
629
+
448
630
  >>
631
+ class Advert < ActiveRecord::Base
632
+ fields do
633
+ body :text
634
+ title :string, :default => "Untitled"
635
+ end
636
+ end
637
+ {.hidden}
449
638
 
450
639
  ## Legacy Keys
451
640
 
@@ -468,5 +657,5 @@ HoboFields has some support for legacy keys.
468
657
  Cleanup
469
658
  {.hidden}
470
659
 
471
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
660
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
472
661
  {.hidden}
@@ -12,12 +12,17 @@ Firstly, in order to test the migration generator outside of a full Rails stack,
12
12
 
13
13
  We also need to get ActiveRecord set up with a database connection
14
14
 
15
+ >> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
16
+ >> mysql_user = 'root'; mysql_password = ''
17
+ >> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
15
18
  >> mysql_database = "hobofields_doctest"
16
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
17
- >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
18
- >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
19
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
20
+ >> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
21
+ >> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
19
22
  :database => mysql_database,
20
- :host => "localhost")
23
+ :host => "localhost",
24
+ :username => mysql_user,
25
+ :password => mysql_password)
21
26
 
22
27
  Some load path manipulation you shouldn't need:
23
28
 
@@ -91,4 +96,4 @@ We'll define a method to make that easier next time
91
96
 
92
97
  ## Cleanup
93
98
 
94
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
99
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
@@ -17,12 +17,15 @@ ActiveRecord 2.3.2 doesn't work very well without a connection, even
17
17
  though we don't need it for this test:
18
18
  {.hidden}
19
19
 
20
+ >> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
21
+ >> mysql_user = 'root'; mysql_password = ''
22
+ >> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
20
23
  >> mysql_database = "hobofields_doctest"
21
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
22
- >> system("mysqladmin create #{mysql_database}") or raise "could not create database"
24
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
25
+ >> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
23
26
  >> ActiveRecord::Base.establish_connection(:adapter => "mysql",
24
- :database => mysql_database,
25
- :host => "localhost")
27
+ :database => mysql_database,
28
+ :host => "localhost", :user => mysql_user, :password => mysql_password)
26
29
  {.hidden}
27
30
 
28
31
  Some load path manipulation you shouldn't need:
@@ -100,7 +103,7 @@ Provides validation of correct email address format.
100
103
  >> !!bad.valid?
101
104
  => false
102
105
  >> bad.validate
103
- => "is not valid"
106
+ => "is invalid"
104
107
 
105
108
  ### `HoboFields::HtmlString`
106
109
 
@@ -234,23 +237,6 @@ Sometimes it's nice to have a proper type name. Here's one way you might go abou
234
237
  ## Cleanup
235
238
  {.hidden}
236
239
 
237
- >> system "mysqladmin --force drop #{mysql_database} 2> /dev/null"
240
+ >> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
238
241
  {.hidden}
239
242
 
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
@@ -1,6 +1,9 @@
1
1
  require 'rubygems'
2
2
  require 'rails_generator'
3
3
 
4
+ # we get a SystemStackError on JRuby
5
+ exit(0) if defined? JRUBY_VERSION
6
+
4
7
  require File.join(File.dirname(__FILE__), "test_generator_helper.rb")
5
8
 
6
9
  class TestHobofieldModelGenerator < Test::Unit::TestCase
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hobofields
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.10
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Locke
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-10-15 00:00:00 +01:00
12
+ date: 2009-11-17 00:00:00 +00:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,7 +30,7 @@ dependencies:
30
30
  requirements:
31
31
  - - "="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.8.10
33
+ version: 0.9.0
34
34
  version:
35
35
  description:
36
36
  email: tom@tomlocke.com
@@ -54,6 +54,7 @@ files:
54
54
  - lib/hobo_fields/field_spec.rb
55
55
  - lib/hobo_fields/fields_declaration.rb
56
56
  - lib/hobo_fields/html_string.rb
57
+ - lib/hobo_fields/index_spec.rb
57
58
  - lib/hobo_fields/markdown_string.rb
58
59
  - lib/hobo_fields/migration_generator.rb
59
60
  - lib/hobo_fields/model_extensions.rb
@@ -65,6 +66,7 @@ files:
65
66
  - lib/hobo_fields/text.rb
66
67
  - lib/hobo_fields/textile_string.rb
67
68
  - lib/hobofields.rb
69
+ - rails_generators/hobo_migration/USAGE
68
70
  - rails_generators/hobo_migration/hobo_migration_generator.rb
69
71
  - rails_generators/hobo_migration/templates/migration.rb
70
72
  - rails_generators/hobofield_model/USAGE
@@ -104,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
106
  version:
105
107
  requirements: []
106
108
 
107
- rubyforge_project: hobofields
109
+ rubyforge_project: hobo
108
110
  rubygems_version: 1.3.5
109
111
  signing_key:
110
112
  specification_version: 3