Skip to content

Commit

Permalink
Merge branch 'master' into set_binds
Browse files Browse the repository at this point in the history
* master:
  don't establish a new connection when testing with `sqlite3_mem`.
  sqlite >= 3.8.0 supports partial indexes
  Don't try to get the subclass if the inheritance column doesn't exist
  Enum mappings are now exposed via class methods instead of constants.
  Fix fields_for documentation with index option [ci skip]
  quick pass through Active Record CHANGELOG. [ci skip]
  [ci skip] Grammar correction
  single quotes for controller generated routes
  [ci skip] Added alias to CSRF
  Set NameError#name
  • Loading branch information
tenderlove committed Jan 14, 2014
2 parents c7cf7f4 + b233307 commit 09035e6
Show file tree
Hide file tree
Showing 19 changed files with 163 additions and 59 deletions.
51 changes: 45 additions & 6 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,40 @@
* Enable partial indexes for sqlite >= 3.8.0

See http://www.sqlite.org/partialindex.html

*Cody Cutrer*

* Don't try to get the subclass if the inheritance column doesn't exist

The `subclass_from_attrs` method is called even if the column specified by
the `inheritance_column` setting doesn't exist. This prevents setting associations
via the attributes hash if the association name clashes with the value of the setting,
typically `:type`. This worked previously in Rails 3.2.

*Ujjwal Thaakar*

* Enum mappings are now exposed via class methods instead of constants.

Example:

class Conversation < ActiveRecord::Base
enum status: [ :active, :archived ]
end

Before:

Conversation::STATUS # => { "active" => 0, "archived" => 1 }

After:

Conversation.statuses # => { "active" => 0, "archived" => 1 }

*Godfrey Chan*

* Set `NameError#name` when STI-class-lookup fails.

*Chulki Lee*

* Fix bug in `becomes!` when changing from the base model to a STI sub-class. * Fix bug in `becomes!` when changing from the base model to a STI sub-class.


Fixes #13272. Fixes #13272.
Expand Down Expand Up @@ -61,7 +98,7 @@
class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`. class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`.


To understand the exact behavior of this class, it is best to review the To understand the exact behavior of this class, it is best to review the
behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb` behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`.


*Richard Schneeman* *Richard Schneeman*


Expand Down Expand Up @@ -407,7 +444,7 @@
*kostya*, *Lauro Caetano* *kostya*, *Lauro Caetano*


* `type_to_sql` returns a `String` for unmapped columns. This fixes an error * `type_to_sql` returns a `String` for unmapped columns. This fixes an error
when using unmapped array types in PG when using unmapped PostgreSQL array types.


Example: Example:


Expand Down Expand Up @@ -446,7 +483,7 @@


* Update counter cache on a `has_many` relationship regardless of default scope. * Update counter cache on a `has_many` relationship regardless of default scope.


Fix #12952. Fixes #12952.


*Uku Taht* *Uku Taht*


Expand All @@ -457,9 +494,10 @@


*Cody Cutrer*, *Yves Senn* *Cody Cutrer*, *Yves Senn*


* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child marked with `dependent: destroy` fails to be destroyed. * Raise `ActiveRecord::RecordNotDestroyed` when a replaced child
marked with `dependent: destroy` fails to be destroyed.


Fix #12812 Fixex #12812.


*Brian Thomas Storti* *Brian Thomas Storti*


Expand Down Expand Up @@ -1365,6 +1403,7 @@
*Yves Senn* *Yves Senn*


* Fix the `:primary_key` option for `has_many` associations. * Fix the `:primary_key` option for `has_many` associations.

Fixes #10693. Fixes #10693.


*Yves Senn* *Yves Senn*
Expand Down Expand Up @@ -1489,7 +1528,7 @@


* Trigger a save on `has_one association=(associate)` when the associate contents have changed. * Trigger a save on `has_one association=(associate)` when the associate contents have changed.


Fix #8856. Fixes #8856.


*Chris Thompson* *Chris Thompson*


Expand Down
Expand Up @@ -155,6 +155,10 @@ def supports_savepoints?
true true
end end


def supports_partial_index?
sqlite_version >= '3.8.0'
end

# Returns true, since this connection adapter supports prepared statement # Returns true, since this connection adapter supports prepared statement
# caching. # caching.
def supports_statement_cache? def supports_statement_cache?
Expand Down Expand Up @@ -397,13 +401,25 @@ def columns(table_name) #:nodoc:
# Returns an array of indexes for the given table. # Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc: def indexes(table_name, name = nil) #:nodoc:
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
sql = <<-SQL
SELECT sql
FROM sqlite_master
WHERE name=#{quote(row['name'])} AND type='index'
UNION ALL
SELECT sql
FROM sqlite_temp_master
WHERE name=#{quote(row['name'])} AND type='index'
SQL
index_sql = exec_query(sql).first['sql']
match = /\sWHERE\s+(.+)$/i.match(index_sql)
where = match[1] if match
IndexDefinition.new( IndexDefinition.new(
table_name, table_name,
row['name'], row['name'],
row['unique'] != 0, row['unique'] != 0,
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
col['name'] col['name']
}) }, nil, nil, where)
end end
end end


Expand Down
17 changes: 10 additions & 7 deletions activerecord/lib/active_record/enum.rb
Expand Up @@ -56,21 +56,24 @@ module ActiveRecord
# In rare circumstances you might need to access the mapping directly. # In rare circumstances you might need to access the mapping directly.
# The mappings are exposed through a constant with the attributes name: # The mappings are exposed through a constant with the attributes name:
# #
# Conversation::STATUS # => { "active" => 0, "archived" => 1 } # Conversation.statuses # => { "active" => 0, "archived" => 1 }
# #
# Use that constant when you need to know the ordinal value of an enum: # Use that constant when you need to know the ordinal value of an enum:
# #
# Conversation.where("status <> ?", Conversation::STATUS[:archived]) # Conversation.where("status <> ?", Conversation.statuses[:archived])
module Enum module Enum
def enum(definitions) def enum(definitions)
klass = self klass = self
definitions.each do |name, values| definitions.each do |name, values|
# STATUS = { } # statuses = { }
enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym name = name.to_sym


# def self.statuses statuses end
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }

_enum_methods_module.module_eval do _enum_methods_module.module_eval do
# def status=(value) self[:status] = STATUS[value] end # def status=(value) self[:status] = statuses[value] end
define_method("#{name}=") { |value| define_method("#{name}=") { |value|
if enum_values.has_key?(value) || value.blank? if enum_values.has_key?(value) || value.blank?
self[name] = enum_values[value] self[name] = enum_values[value]
Expand All @@ -84,10 +87,10 @@ def enum(definitions)
end end
} }


# def status() STATUS.key self[:status] end # def status() statuses.key self[:status] end
define_method(name) { enum_values.key self[name] } define_method(name) { enum_values.key self[name] }


# def status_before_type_cast() STATUS.key self[:status] end # def status_before_type_cast() statuses.key self[:status] end
define_method("#{name}_before_type_cast") { enum_values.key self[name] } define_method("#{name}_before_type_cast") { enum_values.key self[name] }


pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
Expand Down
24 changes: 16 additions & 8 deletions activerecord/lib/active_record/inheritance.rb
Expand Up @@ -18,13 +18,17 @@ def new(*args, &block)
if abstract_class? || self == Base if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end end
if (attrs = args.first).is_a?(Hash)
if subclass = subclass_from_attrs(attrs) attrs = args.first
return subclass.new(*args, &block) if subclass_from_attributes?(attrs)
end subclass = subclass_from_attributes(attrs)
end

if subclass
subclass.new(*args, &block)
else
super
end end
# Delegate to the original .new
super
end end


# Returns +true+ if this does not need STI type condition. Returns # Returns +true+ if this does not need STI type condition. Returns
Expand Down Expand Up @@ -126,7 +130,7 @@ def compute_type(type_name)
end end
end end


raise NameError, "uninitialized constant #{candidates.first}" raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
end end
end end


Expand Down Expand Up @@ -172,7 +176,11 @@ def type_condition(table = arel_table)
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
# If this is a StrongParameters hash, and access to inheritance_column is not permitted, # If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil # this will ignore the inheritance column and return nil
def subclass_from_attrs(attrs) def subclass_from_attributes?(attrs)
columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
end

def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column] subclass_name = attrs.with_indifferent_access[inheritance_column]


if subclass_name.present? && subclass_name != self.name if subclass_name.present? && subclass_name != self.name
Expand Down
36 changes: 21 additions & 15 deletions activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
Expand Up @@ -34,24 +34,30 @@ def test_bad_connection
end end


def test_connect_with_url def test_connect_with_url
original_connection = ActiveRecord::Base.remove_connection skip "can't establish new connection when using memory db" if in_memory_db?
tf = Tempfile.open 'whatever' begin
url = "sqlite3://#{tf.path}" original_connection = ActiveRecord::Base.remove_connection
ActiveRecord::Base.establish_connection(url) tf = Tempfile.open 'whatever'
assert ActiveRecord::Base.connection url = "sqlite3://#{tf.path}"
ensure ActiveRecord::Base.establish_connection(url)
tf.close assert ActiveRecord::Base.connection
tf.unlink ensure
ActiveRecord::Base.establish_connection(original_connection) tf.close
tf.unlink
ActiveRecord::Base.establish_connection(original_connection)
end
end end


def test_connect_memory_with_url def test_connect_memory_with_url
original_connection = ActiveRecord::Base.remove_connection skip "can't establish new connection when using memory db" if in_memory_db?
url = "sqlite3:///:memory:" begin
ActiveRecord::Base.establish_connection(url) original_connection = ActiveRecord::Base.remove_connection
assert ActiveRecord::Base.connection url = "sqlite3:///:memory:"
ensure ActiveRecord::Base.establish_connection(url)
ActiveRecord::Base.establish_connection(original_connection) assert ActiveRecord::Base.connection
ensure
ActiveRecord::Base.establish_connection(original_connection)
end
end end


def test_valid_column def test_valid_column
Expand Down
4 changes: 3 additions & 1 deletion activerecord/test/cases/base_test.rb
Expand Up @@ -1301,9 +1301,11 @@ def test_compute_type_success
end end


def test_compute_type_nonexistent_constant def test_compute_type_nonexistent_constant
assert_raises NameError do e = assert_raises NameError do
ActiveRecord::Base.send :compute_type, 'NonexistentModel' ActiveRecord::Base.send :compute_type, 'NonexistentModel'
end end
assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
end end


def test_compute_type_no_method_error def test_compute_type_no_method_error
Expand Down
6 changes: 3 additions & 3 deletions activerecord/test/cases/enum_test.rb
Expand Up @@ -74,9 +74,9 @@ class EnumTest < ActiveRecord::TestCase
end end


test "constant to access the mapping" do test "constant to access the mapping" do
assert_equal 0, Book::STATUS[:proposed] assert_equal 0, Book.statuses[:proposed]
assert_equal 1, Book::STATUS["written"] assert_equal 1, Book.statuses["written"]
assert_equal 2, Book::STATUS[:published] assert_equal 2, Book.statuses[:published]
end end


test "building new objects with enum scopes" do test "building new objects with enum scopes" do
Expand Down
9 changes: 8 additions & 1 deletion activerecord/test/cases/inheritance_test.rb
@@ -1,10 +1,11 @@
require "cases/helper" require 'cases/helper'
require 'models/company' require 'models/company'
require 'models/person' require 'models/person'
require 'models/post' require 'models/post'
require 'models/project' require 'models/project'
require 'models/subscriber' require 'models/subscriber'
require 'models/vegetables' require 'models/vegetables'
require 'models/shop'


class InheritanceTest < ActiveRecord::TestCase class InheritanceTest < ActiveRecord::TestCase
fixtures :companies, :projects, :subscribers, :accounts, :vegetables fixtures :companies, :projects, :subscribers, :accounts, :vegetables
Expand Down Expand Up @@ -367,4 +368,10 @@ def test_instantiation_doesnt_try_to_require_corresponding_file
ensure ensure
ActiveRecord::Base.store_full_sti_class = true ActiveRecord::Base.store_full_sti_class = true
end end

def test_sti_type_from_attributes_disabled_in_non_sti_class
phone = Shop::Product::Type.new(name: 'Phone')
product = Shop::Product.new(:type => phone)
assert product.save
end
end end
2 changes: 2 additions & 0 deletions activerecord/test/cases/schema_dumper_test.rb
Expand Up @@ -190,6 +190,8 @@ def test_schema_dumps_partial_indices
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
else else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end end
Expand Down
5 changes: 5 additions & 0 deletions activerecord/test/models/shop.rb
Expand Up @@ -5,6 +5,11 @@ class Collection < ActiveRecord::Base


class Product < ActiveRecord::Base class Product < ActiveRecord::Base
has_many :variants, :dependent => :delete_all has_many :variants, :dependent => :delete_all
belongs_to :type

class Type < ActiveRecord::Base
has_many :products
end
end end


class Variant < ActiveRecord::Base class Variant < ActiveRecord::Base
Expand Down
5 changes: 5 additions & 0 deletions activerecord/test/schema/schema.rb
Expand Up @@ -557,9 +557,14 @@ def create_table(*args, &block)


create_table :products, force: true do |t| create_table :products, force: true do |t|
t.references :collection t.references :collection
t.references :type
t.string :name t.string :name
end end


create_table :product_types, force: true do |t|
t.string :name
end

create_table :projects, force: true do |t| create_table :projects, force: true do |t|
t.string :name t.string :name
t.string :type t.string :type
Expand Down
Expand Up @@ -105,7 +105,7 @@ class << self
# hash = Hash.from_xml(xml) # hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}} # # => {"hash"=>{"foo"=>1, "bar"=>2}}
# #
# DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or # DisallowedType is raised if the XML contains attributes with <tt>type="yaml"</tt> or
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.
def from_xml(xml, disallowed_types = nil) def from_xml(xml, disallowed_types = nil)
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
Expand Down
13 changes: 10 additions & 3 deletions guides/source/form_helpers.md
Expand Up @@ -751,7 +751,7 @@ You might want to render a form with a set of edit fields for each of a person's
<%= form_for @person do |person_form| %> <%= form_for @person do |person_form| %>
<%= person_form.text_field :name %> <%= person_form.text_field :name %>
<% @person.addresses.each do |address| %> <% @person.addresses.each do |address| %>
<%= person_form.fields_for address, index: address do |address_form|%> <%= person_form.fields_for address, index: address.id do |address_form|%>
<%= address_form.text_field :city %> <%= address_form.text_field :city %>
<% end %> <% end %>
<% end %> <% end %>
Expand All @@ -774,9 +774,16 @@ This will result in a `params` hash that looks like
{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}} {'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
``` ```


Rails knows that all these inputs should be part of the person hash because you called `fields_for` on the first form builder. By specifying an `:index` option you're telling Rails that instead of naming the inputs `person[address][city]` it should insert that index surrounded by [] between the address and the city. If you pass an Active Record object as we did then Rails will call `to_param` on it, which by default returns the database id. This is often useful as it is then easy to locate which Address record should be modified. You can pass numbers with some other significance, strings or even `nil` (which will result in an array parameter being created). Rails knows that all these inputs should be part of the person hash because you
called `fields_for` on the first form builder. By specifying an `:index` option
you're telling Rails that instead of naming the inputs `person[address][city]`
it should insert that index surrounded by [] between the address and the city.
This is often useful as it is then easy to locate which Address record
should be modified. You can pass numbers with some other significance,
strings or even `nil` (which will result in an array parameter being created).


To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example To create more intricate nestings, you can specify the first part of the input
name (`person[address]` in the previous example) explicitly:


```erb ```erb
<%= fields_for 'person[address][primary]', address, index: address do |address_form| %> <%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
Expand Down

0 comments on commit 09035e6

Please sign in to comment.