aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG.md4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb34
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb70
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb32
-rw-r--r--activerecord/lib/active_record/type/date_time.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb11
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb15
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb17
-rw-r--r--railties/CHANGELOG.md14
-rw-r--r--railties/lib/rails/generators/app_base.rb21
-rw-r--r--railties/lib/rails/rack/log_tailer.rb4
-rw-r--r--railties/test/generators/app_generator_test.rb15
-rw-r--r--railties/test/railties/railtie_test.rb2
16 files changed, 251 insertions, 64 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index fc726d6519..823597fc92 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Detect in-place modifications of PG array types
+
+ *Sean Griffin*
+
* Add `bin/rake db:purge` task to empty the current database.
*Yves Senn*
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index 8f6247c7d2..a865c5c310 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -3,7 +3,11 @@ module ActiveRecord
module PostgreSQL
module Cast # :nodoc:
def point_to_string(point) # :nodoc:
- "(#{point[0]},#{point[1]})"
+ "(#{number_for_point(point[0])},#{number_for_point(point[1])})"
+ end
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
end
def hstore_to_string(object, array_member = false) # :nodoc:
@@ -38,21 +42,6 @@ module ActiveRecord
end
end
- def array_to_string(value, column, adapter) # :nodoc:
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
def range_to_string(object) # :nodoc:
from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
@@ -86,19 +75,6 @@ module ActiveRecord
end
end
end
-
- ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
-
- def quote_and_escape(value)
- case value
- when "NULL", Numeric
- value
- else
- value = value.gsub(/\\/, ARRAY_ESCAPE)
- value.gsub!(/"/,"\\\"")
- "\"#{value}\""
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index 4e7d472d97..d322c56acc 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -3,11 +3,26 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value
- attr_reader :subtype
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
delegate :type, to: :subtype
- def initialize(subtype)
+ def initialize(subtype, delimiter = ',')
@subtype = subtype
+ @delimiter = delimiter
end
def type_cast_from_database(value)
@@ -22,16 +37,12 @@ module ActiveRecord
type_cast_array(value, :type_cast_from_user)
end
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- require 'active_record/connection_adapters/postgresql/array_parser'
- include PostgreSQL::ArrayParser
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
end
private
@@ -43,6 +54,41 @@ module ActiveRecord
@subtype.public_send(method, value)
end
end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 9007bfb178..86277c5542 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -3,20 +3,33 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Point < Type::Value
+ include Type::Mutable
+
def type
:point
end
def type_cast(value)
- if ::String === value
+ case value
+ when ::String
if value[0] == '(' && value[-1] == ')'
value = value[1...-1]
end
- value.split(',').map{ |v| Float(v) }
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
else
value
end
end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ PostgreSQLColumn.point_to_string(value)
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 28f7a4eafb..e396ff4a1e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -40,7 +40,7 @@ module ActiveRecord
def register_array_type(row)
if subtype = @store.lookup(row['typelem'].to_i)
- register row['oid'], OID::Array.new(subtype)
+ register row['oid'], OID::Array.new(subtype, row['typdelim'])
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 3cf40e6cd4..17fabe5af6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -32,11 +32,7 @@ module ActiveRecord
when 'point' then super(PostgreSQLColumn.point_to_string(value))
when 'json' then super(PostgreSQLColumn.json_to_string(value))
else
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
- else
- super
- end
+ super(value, array_column(column))
end
when Hash
case sql_type
@@ -98,11 +94,7 @@ module ActiveRecord
when 'point' then PostgreSQLColumn.point_to_string(value)
when 'json' then PostgreSQLColumn.json_to_string(value)
else
- if column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- else
- super(value, column)
- end
+ super(value, array_column(column))
end
when Hash
case column.sql_type
@@ -185,6 +177,26 @@ module ActiveRecord
super
end
end
+
+ def array_column(column)
+ if column.array && !column.respond_to?(:type_cast_for_database)
+ OID::Array.new(AdapterProxyType.new(column, self))
+ else
+ column
+ end
+ end
+
+ class AdapterProxyType < SimpleDelegator
+ def initialize(column, adapter)
+ @column = column
+ @adapter = adapter
+ super(column)
+ end
+
+ def type_cast_for_database(value)
+ @adapter.type_cast(value, @column)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 560d63c101..5f19608a33 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -7,6 +7,16 @@ module ActiveRecord
:datetime
end
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
private
def cast_value(string)
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index bdd2f4f4f9..e9522d5956 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -3,6 +3,7 @@ require "cases/helper"
class PostgresqlArrayTest < ActiveRecord::TestCase
include InTimeZone
+ OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
@@ -10,11 +11,18 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
t.string 'tags', array: true
t.integer 'ratings', array: true
t.datetime :datetimes, array: true
+ t.hstore :hstores, array: true
end
end
@column = PgArray.columns_hash['tags']
@@ -200,6 +208,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal tags, ar.tags
end
+ def test_string_quoting_rules_match_pg_behavior
+ tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
+ x = PgArray.create!(tags: tags)
+ x.reload
+
+ assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ end
+
+ def test_quoting_non_standard_delimiters
+ strings = ["hello,", "world;"]
+ comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+
+ assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ end
+
+ def test_mutate_array
+ x = PgArray.create!(tags: %w(one two))
+
+ x.tags << "three"
+ x.save!
+ x.reload
+
+ assert_equal %w(one two three), x.tags
+ assert_not x.changed?
+ end
+
+ def test_mutate_value_in_array
+ x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+
+ x.hstores.first['a'] = 'c'
+ x.save!
+ x.reload
+
+ assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
+ assert_not x.changed?
+ end
+
def test_datetime_with_timezone_awareness
tz = "Pacific Time (US & Canada)"
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 8dfc452cf2..faf195783d 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -59,4 +59,15 @@ class PostgresqlPointTest < ActiveRecord::TestCase
assert record.reload
assert_equal [1.1, 2.2], record.x
end
+
+ def test_mutation
+ p = PostgresqlPoint.create! x: [10, 20]
+
+ p.x[1] = 25
+ p.save!
+ p.reload
+
+ assert_equal [10.0, 25.0], p.x
+ assert_not p.changed?
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
new file mode 100644
index 0000000000..23817198b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -0,0 +1,15 @@
+require 'cases/helper'
+
+class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "array delimiters are looked up correctly" do
+ box_array = @connection.type_map.lookup(1020)
+ int_array = @connection.type_map.lookup(1007)
+
+ assert_equal ';', box_array.delimiter
+ assert_equal ',', int_array.delimiter
+ end
+end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index a925cf4c05..1c0134843b 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -157,6 +157,23 @@ module ActiveRecord
assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
end
+ def test_invert_change_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column, [:table, :column, :type, {}]
+ end
+ end
+
+ def test_invert_change_column_default
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ end
+ end
+
+ def test_invert_change_column_null
+ add = @recorder.inverse_of :change_column_null, [:table, :column, true]
+ assert_equal [:change_column_null, [:table, :column, false]], add
+ end
+
def test_invert_remove_column
add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
assert_equal [:add_column, [:table, :column, :type, {}], nil], add
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 3350b1a4b2..c33a4ed192 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,17 @@
+* Deprecate `Rails::Rack::LogTailer` with not replacement.
+
+ *Rafael Mendonça França*
+
+* Add a generic --skip-gems options to generator
+
+ This option is useful if users want to remove some gems like jbuilder,
+ turbolinks, coffee-rails, etc that don't have specific options on the
+ generator.
+
+ rails new my_app --skip-gems turbolinks coffee-rails
+
+ *Rafael Mendonça França*
+
* Invalid `bin/rails generate` commands will now show spelling suggestions.
*Richard Schneeman*
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 569afe8104..76f8a1b816 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -41,6 +41,9 @@ module Rails
class_option :skip_active_record, type: :boolean, aliases: '-O', default: false,
desc: 'Skip Active Record files'
+ class_option :skip_gems, type: :array, default: [],
+ desc: 'Skip the provided gems files'
+
class_option :skip_action_view, type: :boolean, aliases: '-V', default: false,
desc: 'Skip Action View files'
@@ -79,7 +82,7 @@ module Rails
end
def initialize(*args)
- @gem_filter = lambda { |gem| true }
+ @gem_filter = lambda { |gem| !options[:skip_gems].include?(gem.name) }
@extra_entries = []
super
convert_database_option_for_jruby
@@ -104,14 +107,14 @@ module Rails
end
def gemfile_entries
- [ rails_gemfile_entry,
- database_gemfile_entry,
- assets_gemfile_entry,
- javascript_gemfile_entry,
- jbuilder_gemfile_entry,
- sdoc_gemfile_entry,
- spring_gemfile_entry,
- @extra_entries].flatten.find_all(&@gem_filter)
+ [rails_gemfile_entry,
+ database_gemfile_entry,
+ assets_gemfile_entry,
+ javascript_gemfile_entry,
+ jbuilder_gemfile_entry,
+ sdoc_gemfile_entry,
+ spring_gemfile_entry,
+ @extra_entries].flatten.find_all(&@gem_filter)
end
def add_gem_entry_filter
diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb
index 50d0eb96fc..bc26421a9e 100644
--- a/railties/lib/rails/rack/log_tailer.rb
+++ b/railties/lib/rails/rack/log_tailer.rb
@@ -1,7 +1,11 @@
+require 'active_support/deprecation'
+
module Rails
module Rack
class LogTailer
def initialize(app, log = nil)
+ ActiveSupport::Deprecation.warn "LogTailer is deprecated and will be removed on Rails 5"
+
@app = app
path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 74cff08676..2ac5410960 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -448,6 +448,21 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_generator_if_skip_gems_is_given
+ run_generator [destination_root, "--skip-gems", "turbolinks", "coffee-rails"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/turbolinks/, content)
+ assert_no_match(/coffee-rails/, content)
+ end
+ assert_file "app/views/layouts/application.html.erb" do |content|
+ assert_no_match(/data-turbolinks-track/, content)
+ end
+ assert_file "app/assets/javascripts/application.js" do |content|
+ assert_no_match(/turbolinks/, content)
+ end
+ end
+
def test_gitignore_when_sqlite3
run_generator
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index a458240d2f..5042d628cf 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -73,7 +73,7 @@ module RailtiesTest
end
test "railtie have access to application in before_configuration callbacks" do
- $after_initialize = false
+ $before_configuration = false
class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = Rails.root.to_path } ; end
assert_not $before_configuration
require "#{app_path}/config/environment"