Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

When generating a model/migration from cli added ability to specify whether particular property should also have an index #2555

Closed
wants to merge 5 commits into from

7 participants

@german

When generating a model/migration from cli wouldn't that be nice if there would be an ability to specify whether particular property should also have an index like this:

rails g model person name:string:index profile:string

so developer could just run 'rake db:migrate' w/o looking into migration file?

The main change is adding the has_index property in the railties/lib/rails/generators/generated_attribute.rb:

module Rails
  module Generators
    class GeneratedAttribute
      attr_accessor :name, :type, :has_index

      def initialize(name, type, has_index = false)
        type = :string if type.blank?
        @name, @type, @has_index = name, type.to_sym, has_index.eql?("index")
      end

      ...

      def has_index?
        @has_index
      end
    end
  end
end

What do you think of it? Was it worth implementing?

UPD: added ability to specify :limit and :precision/:scale column options in migration like this: name:string[40] or name:string[40]:index or price:decimal[5.2] as proposed below (I chosen square brackets because parentheses break bash shell)

UPD changed a dot to a comma and square brackets to curly ones while specifying column type options

@german german added ability to specify from cli when generating a model/migration w…
…hether particular property should be an index like this 'rails g model person name:string:index profile:string'
3d2eed7
railties/lib/rails/generators/named_base.rb
@@ -154,8 +154,8 @@ module Rails
# Convert attributes array into GeneratedAttribute objects.
def parse_attributes! #:nodoc:
self.attributes = (attributes || []).map do |key_value|
- name, type = key_value.split(':')
- Rails::Generators::GeneratedAttribute.new(name, type)
+ name, type, has_index = key_value.split(':')
@vijaydev Collaborator

Rails 3.2 (edge) has a change where the type can be omitted and it will default to string. So you need to support "name:index" in addition to "name:string:index".

@vijaydev Collaborator

Here is the relevant commit: 08983fe

@german
german added a note

yes, I see

@german
german added a note

fixed in german@12dcac7

@josevalim Owner

What if we remove this code from here and move it to Rails::Generators::GeneratedAttribute.parse(key_value). That said, the responsibility ends up all in one place.

@german
german added a note

You mean move this code to Rails::Generators::GeneratedAttribute#parse(key_value):

   name, type, has_index = key_value.split(':')
   # if user provided "name:index" instead of "name:string:index" type should be set blank
   # so GeneratedAttribute's constructor could set it to :string
   if type.eql?("index")
     has_index = type
     type = nil
   end

So we could write Rails::Generators::GeneratedAttribute.parse(key_value) ? Sure, I think it's ok.

@josevalim Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@german german added condition to check whether user omitted type of the attribute b…
…ut specified it should have index while generating model/migration from the cli, so 'rails g model person name:index age:integer' should work
12dcac7
@alexeymuranov

I like the idea. It would also be nice to be able to specify the length as name:string(40).

@german

@alexeymuranov yeah, I didn't thought of that. Will try to implement it.

@german

@alexeymuranov I think that's what you've talked about: name:string[40] or name:string[40]:index or price:decimal[5.2]. I choose square brackets because parentheses break bash shell

@alexeymuranov

Thanks!

@sikachu
Collaborator

Square brackets need to be escaped in zsh as well ...

@german

Thanks for the feedback @sikachu. So what do you think of which delimiter will do ideally? Comma maybe (like "name:string,20") or curly brackets (like "name:string{20}:index") or dot? Or should I delete the last commit at all?

@alexeymuranov

Sorry for starting this :). Maybe underscore? name:string_20:index
Or just name:string20:index

@dmathieu
Collaborator

Is this really useful ? I don't think a lot of people will use it, it's too hard to remember.
And when you want to create a really specific migration with specific indexes, you can still manually edit the file.

@alexeymuranov

About the parentheses: i thought that the parentheses syntax was natural, and this is how annotate_models writes its comments, but if parentheses do not work in bash, i do not know.
In my opinion, there is some use in all this: it is easier to edit several command lines before pasting them into the shell, than edit multiple migration files afterwards.

@german

@dmathieu well, I don't know about :limit/:precision/:scale options but at least :index option might be useful, I often add indices by myself after generating a migration and I thought it could be automated somehow.

@Bodacious

Just noticed this pull request
+1 - great idea

@alexeymuranov

@german, just a thought: use parenthesis () for :limit, and those who need them from the command line, like me, will escape them!

@german

@alexeymuranov I think I'll better create a thread in rails-core google group, so this pull request could attract more attention and gather some more opinions
/ cc @spastorino

@german german closed this
@german german reopened this
railties/lib/rails/generators/generated_attribute.rb
((9 lines not shown))
type = :string if type.blank?
- @name, @type = name, type.to_sym
+ @name, @type, @attr_options, @has_index = name, *parse_type_and_options(type), has_index.eql?("index")
@josevalim Owner

We don't need to do everything in one line. :)

@german
german added a note

yes, I know)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@josevalim josevalim commented on the diff
...rators/active_record/migration/migration_generator.rb
@@ -3,7 +3,7 @@
module ActiveRecord
module Generators
class MigrationGenerator < Base
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type field:type:index"
@josevalim Owner

Let's change the banner to: "field field:type field:type:index". What do you think?

@german
german added a note

Ok, cool. Actually I'm in doubt whether 'name:string[40]' functionality is needed as it was proposed ealier #2555 (comment) ? what do you think?

@josevalim Owner

I think it would be too hard to put all this information in the banner. We should document it in the USAGE though. Now that we use {} for extra information, we could even change the banner to: field[:type][:index] field[:type][:index] and put the remaining information in the USAGE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@josevalim
Owner

I like [] because this is what rake uses.

@josevalim
Owner

Btw, decimal[5.2] meaning precision.scale is not good. I would suggest using decimal[5,2] or removing it.

railties/lib/rails/generators/generated_attribute.rb
@@ -48,6 +48,27 @@ def human_name
def reference?
self.type.in?([:references, :belongs_to])
end
+
+ def has_index?
+ @has_index
+ end
+
+ # parse possible attribute options like :limit for string/text/binary/integer or :precision/:scale for decimals
+ # when declaring options square brackets should be used since bash interpreter fails when parentheses are used
+ def parse_type_and_options(type)
@josevalim Owner

As I said, this could be the class method parse and the object would be then initialized with all four args (name, type, index and options).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@german german changed square brackets to curly ones in column type definition, also…
… added ability to specify (uniq|unique) in index definition, so now it's possible to write 'discount:decimal{5,2}:uniq' or 'user_uuid:uniq' while generating new model/migration
51eded9
@german

I've

  • changed square brackets to the curly ones in column type definition (so zsh and bash are ok)
  • changed a dot to a comma
  • added ability to specify (uniq|unique) in column definition (instead of index)

so now it's possible to write 'discount:decimal{5,2}:unique' or 'user_uuid:uniq' while generating new model/migration.

What do you think of this? I'll investigate on this further tomorrow. Thanks.

@josevalim
Owner

I have merged this to a local branch. I will push it to master soon.

@josevalim josevalim closed this pull request from a commit
@josevalim josevalim Merge branch 'gzip-index' which contains two features:
1) Adding gzip to pages cache, closes #4124

2) Allow scaffold/model/migration generators to accept a "index" and "uniq"
modifiers, as in: "tracking_id:integer:uniq" in order to generate (unique)
indexes. Some types also accept custom options, for instance, you can
specify the precision and scale for decimals as "price:decimal{7,2}".
This feature closes #2555.
0152fe9
@josevalim josevalim closed this in 0152fe9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 17, 2011
  1. @german

    added ability to specify from cli when generating a model/migration w…

    german authored
    …hether particular property should be an index like this 'rails g model person name:string:index profile:string'
  2. @german

    added condition to check whether user omitted type of the attribute b…

    german authored
    …ut specified it should have index while generating model/migration from the cli, so 'rails g model person name:index age:integer' should work
Commits on Sep 2, 2011
  1. @german
Commits on Nov 28, 2011
  1. Merge branch 'master' of git://github.com/rails/rails

    Dmitrii Samoilov authored
Commits on Dec 23, 2011
  1. @german

    changed square brackets to curly ones in column type definition, also…

    german authored
    … added ability to specify (uniq|unique) in index definition, so now it's possible to write 'discount:decimal{5,2}:uniq' or 'user_uuid:uniq' while generating new model/migration
This page is out of date. Refresh to see the latest.
View
2  activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -3,7 +3,7 @@
module ActiveRecord
module Generators
class MigrationGenerator < Base
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type field:type:index"
@josevalim Owner

Let's change the banner to: "field field:type field:type:index". What do you think?

@german
german added a note

Ok, cool. Actually I'm in doubt whether 'name:string[40]' functionality is needed as it was proposed ealier #2555 (comment) ? what do you think?

@josevalim Owner

I think it would be too hard to put all this information in the banner. We should document it in the USAGE though. Now that we use {} for extra information, we could even change the banner to: field[:type][:index] field[:type][:index] and put the remaining information in the USAGE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
def create_migration_file
set_local_assigns!
View
12 activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -2,14 +2,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<%- if migration_action == 'add' -%>
def change
<% attributes.each do |attribute| -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %>
+ add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- if attribute.has_index? -%>
+ add_index :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_index_options %>
+ <%- end %>
<%- end -%>
end
<%- else -%>
def up
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
- <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %>
+ <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
+ <% if attribute.has_index? && migration_action == 'add' %>
+ add_index :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_index_options %>
+ <% end -%>
<%- end -%>
<%- end -%>
end
@@ -17,7 +23,7 @@ def up
def down
<% attributes.reverse.each do |attribute| -%>
<%- if migration_action -%>
- <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %>
+ <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %>
<%- end -%>
<%- end -%>
end
View
2  activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -3,7 +3,7 @@
module ActiveRecord
module Generators
class ModelGenerator < Base
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type field:type:index"
check_class_collision
View
7 activerecord/lib/rails/generators/active_record/model/templates/migration.rb
@@ -2,7 +2,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
def change
create_table :<%= table_name %> do |t|
<% attributes.each do |attribute| -%>
- t.<%= attribute.type %> :<%= attribute.name %>
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% if options[:timestamps] %>
t.timestamps
@@ -10,8 +10,11 @@ def change
end
<% if options[:indexes] -%>
<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
- add_index :<%= table_name %>, :<%= attribute.name %>_id
+ add_index :<%= table_name %>, :<%= attribute.name %>_id<%= attribute.inject_index_options %>
<% end -%>
<% end -%>
+<% attributes.select {|attr| attr.has_index? }.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_index_options %>
+<% end -%>
end
end
View
52 railties/lib/rails/generators/generated_attribute.rb
@@ -4,11 +4,10 @@
module Rails
module Generators
class GeneratedAttribute
- attr_accessor :name, :type
+ attr_accessor :name, :type, :has_index, :attr_options
- def initialize(name, type)
- type = :string if type.blank?
- @name, @type = name, type.to_sym
+ def initialize(column_definition)
+ parse column_definition
end
def field_type
@@ -48,6 +47,51 @@ def human_name
def reference?
self.type.in?([:references, :belongs_to])
end
+
+ def has_index?
+ @has_index
+ end
+
+ def has_uniq_index?
+ @has_uniq_index
+ end
+
+ def parse(column_definition)
+ name, type, has_index = column_definition.split(':')
+ # if user provided "name:index" instead of "name:string:index" type should be set blank
+ # so GeneratedAttribute's constructor could set it to :string
+ if type =~ /index|uniq|unique/i
+ has_index = type
+ type = nil
+ end
+ type = :string if type.blank?
+
+ @name = name
+ @type, @attr_options = *parse_type_and_options(type)
+ @has_index = ['index','uniq','unique'].include?(has_index)
+ @has_uniq_index = ['uniq','unique'].include?(has_index)
+ end
+
+ # parse possible attribute options like :limit for string/text/binary/integer or :precision/:scale for decimals
+ # when declaring options curly brackets should be used
+ def parse_type_and_options(type)
+ attribute_options = case type
+ when /(string|text|binary|integer){(\d+)}/
+ {:limit => $2.to_i}
+ when /decimal{(\d+),(\d+)}/
+ {:precision => $1.to_i, :scale => $2.to_i}
+ else; {}
+ end
+ [type.to_s.gsub(/{.*}/,'').to_sym, attribute_options]
+ end
+
+ def inject_options
+ @attr_options.blank? ? '' : ", #{@attr_options.to_s.gsub(/[{}]/, '')}"
+ end
+
+ def inject_index_options
+ has_uniq_index? ? ", :unique => true" : ''
+ end
end
end
end
View
5 railties/lib/rails/generators/named_base.rb
@@ -153,9 +153,8 @@ def assign_names!(name) #:nodoc:
# Convert attributes array into GeneratedAttribute objects.
def parse_attributes! #:nodoc:
- self.attributes = (attributes || []).map do |key_value|
- name, type = key_value.split(':')
- Rails::Generators::GeneratedAttribute.new(name, type)
+ self.attributes = (attributes || []).map do |attr|
+ Rails::Generators::GeneratedAttribute.new(attr)
end
end
View
2  railties/lib/rails/generators/rails/migration/migration_generator.rb
@@ -1,7 +1,7 @@
module Rails
module Generators
class MigrationGenerator < NamedBase #metagenerator
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type field:type:index"
hook_for :orm, :required => true
end
end
View
2  railties/lib/rails/generators/rails/model/model_generator.rb
@@ -1,7 +1,7 @@
module Rails
module Generators
class ModelGenerator < NamedBase #metagenerator
- argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type field:type:index"
hook_for :orm, :required => true
end
end
View
2  railties/lib/rails/generators/test_case.rb
@@ -219,7 +219,7 @@ def generator(args=self.default_arguments, options={}, config={})
# create_generated_attribute(:string, 'name')
#
def create_generated_attribute(attribute_type, name = 'test')
- Rails::Generators::GeneratedAttribute.new(name, attribute_type.to_s)
+ Rails::Generators::GeneratedAttribute.new([name, attribute_type.to_s].join(':'))
end
protected
View
2  railties/test/generators/generated_attribute_test.rb
@@ -69,7 +69,7 @@ def test_default_value_is_string
end
def test_default_value_for_type
- att = Rails::Generators::GeneratedAttribute.new("type", "string")
+ att = Rails::Generators::GeneratedAttribute.new("type:string")
assert_equal("", att.default)
end
View
62 railties/test/generators/migration_generator_test.rb
@@ -58,6 +58,68 @@ def test_remove_migration_with_attributes
end
end
+ def test_add_migration_with_attributes_and_indices
+ migration = "add_title_with_index_and_body_to_posts"
+ run_generator [migration, "title:string:index", "body:text", "user_id:integer:unique"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |up|
+ assert_match(/add_column :posts, :title, :string/, up)
+ assert_match(/add_column :posts, :body, :text/, up)
+ assert_match(/add_column :posts, :user_id, :integer/, up)
+ end
+ assert_match(/add_index :posts, :title/, content)
+ assert_match(/add_index :posts, :user_id, :unique => true/, content)
+ end
+ end
+
+ def test_add_migration_with_attributes_and_wrong_index_declaration
+ migration = "add_title_and_content_to_books"
+ run_generator [migration, "title:string:inex", "content:text", "user_id:integer:unik"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |up|
+ assert_match(/add_column :books, :title, :string/, up)
+ assert_match(/add_column :books, :content, :text/, up)
+ assert_match(/add_column :books, :user_id, :integer/, up)
+ end
+ assert_not_match(/add_index :books, :title/, content)
+ assert_not_match(/add_index :books, :user_id/, content)
+ end
+ end
+
+ def test_add_migration_with_attributes_without_type_and_index
+ migration = "add_title_with_index_and_body_to_posts"
+ run_generator [migration, "title:index", "body:text", "user_uuid:uniq"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |up|
+ assert_match(/add_column :posts, :title, :string/, up)
+ assert_match(/add_column :posts, :body, :text/, up)
+ assert_match(/add_column :posts, :user_uuid, :string/, up)
+ end
+ assert_match(/add_index :posts, :title/, content)
+ assert_match(/add_index :posts, :user_uuid, :unique => true/, content)
+ end
+ end
+
+ def test_add_migration_with_attributes_index_declaration_and_attribute_options
+ migration = "add_title_and_content_to_books"
+ run_generator [migration, "title:string{40}:index", "content:string{255}", "price:decimal{5,2}:index", "discount:decimal{3,2}:uniq"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |up|
+ assert_match(/add_column :books, :title, :string, :limit=>40/, up)
+ assert_match(/add_column :books, :content, :string, :limit=>255/, up)
+ assert_match(/add_column :books, :price, :decimal, :precision=>5, :scale=>2/, up)
+ assert_match(/add_column :books, :discount, :decimal, :precision=>3, :scale=>2/, up)
+ end
+ assert_match(/add_index :books, :title/, content)
+ assert_match(/add_index :books, :price/, content)
+ assert_match(/add_index :books, :discount, :unique => true/, content)
+ end
+ end
+
def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove
migration = "create_books"
run_generator [migration, "title:string", "content:text"]
View
68 railties/test/generators/model_generator_test.rb
@@ -113,6 +113,74 @@ def test_migration_with_attributes
end
end
+ def test_migration_with_attributes_and_with_index
+ run_generator ["product", "name:string:index", "supplier_id:integer:index", "user_id:integer:uniq", "order_id:unique"]
+
+ assert_migration "db/migrate/create_products.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/create_table :products/, up)
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.integer :supplier_id/, up)
+ assert_match(/t\.integer :user_id/, up)
+ assert_match(/t\.string :order_id/, up)
+
+ assert_match(/add_index :products, :name/, up)
+ assert_match(/add_index :products, :supplier_id/, up)
+ assert_match(/add_index :products, :user_id, :unique => true/, up)
+ assert_match(/add_index :products, :order_id, :unique => true/, up)
+ end
+ end
+ end
+
+ def test_migration_with_attributes_and_with_wrong_index_declaration
+ run_generator ["product", "name:string", "supplier_id:integer:inex", "user_id:integer:unqu"]
+
+ assert_migration "db/migrate/create_products.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/create_table :products/, up)
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.integer :supplier_id/, up)
+ assert_match(/t\.integer :user_id/, up)
+
+ assert_not_match(/add_index :products, :name/, up)
+ assert_not_match(/add_index :products, :supplier_id/, up)
+ assert_not_match(/add_index :products, :user_id/, up)
+ end
+ end
+ end
+
+ def test_migration_with_missing_attribute_type_and_with_index
+ run_generator ["product", "name:index", "supplier_id:integer:index", "year:integer"]
+
+ assert_migration "db/migrate/create_products.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/create_table :products/, up)
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.integer :supplier_id/, up)
+
+ assert_match(/add_index :products, :name/, up)
+ assert_match(/add_index :products, :supplier_id/, up)
+ assert_not_match(/add_index :products, :year/, up)
+ end
+ end
+ end
+
+ def test_add_migration_with_attributes_index_declaration_and_attribute_options
+ run_generator ["product", "title:string{40}:index", "content:string{255}", "price:decimal{5,2}:index", "discount:decimal{5,2}:uniq"]
+
+ assert_migration "db/migrate/create_products.rb" do |content|
+ assert_method :change, content do |up|
+ assert_match(/create_table :products/, up)
+ assert_match(/t.string :title, :limit=>40/, up)
+ assert_match(/t.string :content, :limit=>255/, up)
+ assert_match(/t.decimal :price, :precision=>5, :scale=>2/, up)
+ end
+ assert_match(/add_index :products, :title/, content)
+ assert_match(/add_index :products, :price/, content)
+ assert_match(/add_index :products, :discount, :unique => true/, content)
+ end
+ end
+
def test_migration_without_timestamps
ActiveRecord::Base.timestamped_migrations = false
run_generator ["account"]
Something went wrong with that request. Please try again.