hobofields 0.8.10 → 0.9.0

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