activerecord 3.1.12 → 3.2.22.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +804 -338
- data/README.rdoc +3 -3
- data/examples/performance.rb +20 -1
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/associations/alias_tracker.rb +3 -6
- data/lib/active_record/associations/association.rb +13 -45
- data/lib/active_record/associations/association_scope.rb +3 -15
- data/lib/active_record/associations/belongs_to_association.rb +1 -1
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
- data/lib/active_record/associations/builder/association.rb +6 -4
- data/lib/active_record/associations/builder/belongs_to.rb +7 -4
- data/lib/active_record/associations/builder/collection_association.rb +2 -2
- data/lib/active_record/associations/builder/has_many.rb +4 -4
- data/lib/active_record/associations/builder/has_one.rb +5 -6
- data/lib/active_record/associations/builder/singular_association.rb +3 -16
- data/lib/active_record/associations/collection_association.rb +65 -32
- data/lib/active_record/associations/collection_proxy.rb +8 -41
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
- data/lib/active_record/associations/has_many_association.rb +11 -7
- data/lib/active_record/associations/has_many_through_association.rb +19 -9
- data/lib/active_record/associations/has_one_association.rb +23 -13
- data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
- data/lib/active_record/associations/join_dependency.rb +3 -3
- data/lib/active_record/associations/preloader/through_association.rb +3 -3
- data/lib/active_record/associations/preloader.rb +14 -10
- data/lib/active_record/associations/through_association.rb +8 -4
- data/lib/active_record/associations.rb +92 -76
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +21 -11
- data/lib/active_record/attribute_methods/primary_key.rb +62 -25
- data/lib/active_record/attribute_methods/read.rb +73 -83
- data/lib/active_record/attribute_methods/serialization.rb +120 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
- data/lib/active_record/attribute_methods/write.rb +32 -6
- data/lib/active_record/attribute_methods.rb +231 -30
- data/lib/active_record/autosave_association.rb +44 -26
- data/lib/active_record/base.rb +227 -1708
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
- data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
- data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
- data/lib/active_record/connection_adapters/column.rb +37 -11
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
- data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
- data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
- data/lib/active_record/counter_cache.rb +9 -4
- data/lib/active_record/dynamic_finder_match.rb +12 -0
- data/lib/active_record/dynamic_matchers.rb +84 -0
- data/lib/active_record/errors.rb +11 -1
- data/lib/active_record/explain.rb +86 -0
- data/lib/active_record/explain_subscriber.rb +25 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/fixtures.rb +57 -86
- data/lib/active_record/identity_map.rb +3 -4
- data/lib/active_record/inheritance.rb +174 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locking/optimistic.rb +33 -26
- data/lib/active_record/locking/pessimistic.rb +23 -1
- data/lib/active_record/log_subscriber.rb +8 -4
- data/lib/active_record/migration/command_recorder.rb +8 -8
- data/lib/active_record/migration.rb +68 -35
- data/lib/active_record/model_schema.rb +368 -0
- data/lib/active_record/nested_attributes.rb +60 -24
- data/lib/active_record/persistence.rb +57 -11
- data/lib/active_record/query_cache.rb +6 -6
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +37 -29
- data/lib/active_record/railties/controller_runtime.rb +3 -1
- data/lib/active_record/railties/databases.rake +213 -117
- data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +7 -15
- data/lib/active_record/relation/batches.rb +7 -4
- data/lib/active_record/relation/calculations.rb +55 -16
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +16 -11
- data/lib/active_record/relation/predicate_builder.rb +8 -6
- data/lib/active_record/relation/query_methods.rb +75 -9
- data/lib/active_record/relation/spawn_methods.rb +48 -7
- data/lib/active_record/relation.rb +78 -32
- data/lib/active_record/result.rb +10 -4
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema_dumper.rb +12 -5
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +200 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/serialization.rb +1 -43
- data/lib/active_record/serializers/xml_serializer.rb +4 -45
- data/lib/active_record/session_store.rb +18 -16
- data/lib/active_record/store.rb +52 -0
- data/lib/active_record/test_case.rb +11 -7
- data/lib/active_record/timestamp.rb +17 -3
- data/lib/active_record/transactions.rb +27 -6
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +5 -4
- data/lib/active_record/validations/uniqueness.rb +8 -8
- data/lib/active_record/validations.rb +1 -1
- data/lib/active_record/version.rb +3 -3
- data/lib/active_record.rb +38 -3
- data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
- data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
- data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
- data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
- metadata +49 -28
- data/lib/active_record/named_scope.rb +0 -200
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
-
require 'active_support/core_ext/kernel/requires'
|
3
2
|
require 'active_record/connection_adapters/statement_pool'
|
4
3
|
require 'active_support/core_ext/string/encoding'
|
5
4
|
require 'arel/visitors/bind_visitor'
|
@@ -8,26 +7,11 @@ module ActiveRecord
|
|
8
7
|
module ConnectionAdapters #:nodoc:
|
9
8
|
class SQLiteColumn < Column #:nodoc:
|
10
9
|
class << self
|
11
|
-
def string_to_binary(value)
|
12
|
-
value.gsub(/\0|\%/n) do |b|
|
13
|
-
case b
|
14
|
-
when "\0" then "%00"
|
15
|
-
when "%" then "%25"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
10
|
def binary_to_string(value)
|
21
11
|
if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
|
22
12
|
value = value.force_encoding(Encoding::ASCII_8BIT)
|
23
13
|
end
|
24
|
-
|
25
|
-
value.gsub(/%00|%25/n) do |b|
|
26
|
-
case b
|
27
|
-
when "%00" then "\0"
|
28
|
-
when "%25" then "%"
|
29
|
-
end
|
30
|
-
end
|
14
|
+
value
|
31
15
|
end
|
32
16
|
end
|
33
17
|
end
|
@@ -95,15 +79,11 @@ module ActiveRecord
|
|
95
79
|
@statements = StatementPool.new(@connection,
|
96
80
|
config.fetch(:statement_limit) { 1000 })
|
97
81
|
@config = config
|
98
|
-
end
|
99
|
-
|
100
|
-
def self.visitor_for(pool) # :nodoc:
|
101
|
-
config = pool.spec.config
|
102
82
|
|
103
83
|
if config.fetch(:prepared_statements) { true }
|
104
|
-
Arel::Visitors::SQLite.new
|
84
|
+
@visitor = Arel::Visitors::SQLite.new self
|
105
85
|
else
|
106
|
-
BindSubstitution.new
|
86
|
+
@visitor = BindSubstitution.new self
|
107
87
|
end
|
108
88
|
end
|
109
89
|
|
@@ -137,6 +117,11 @@ module ActiveRecord
|
|
137
117
|
true
|
138
118
|
end
|
139
119
|
|
120
|
+
# Returns true.
|
121
|
+
def supports_explain?
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
140
125
|
def requires_reloading?
|
141
126
|
true
|
142
127
|
end
|
@@ -169,6 +154,10 @@ module ActiveRecord
|
|
169
154
|
sqlite_version >= '3.1.0'
|
170
155
|
end
|
171
156
|
|
157
|
+
def supports_index_sort_order?
|
158
|
+
sqlite_version >= '3.3.0'
|
159
|
+
end
|
160
|
+
|
172
161
|
def native_database_types #:nodoc:
|
173
162
|
{
|
174
163
|
:primary_key => default_primary_key_type,
|
@@ -216,7 +205,7 @@ module ActiveRecord
|
|
216
205
|
value = super
|
217
206
|
if column.type == :string && value.encoding == Encoding::ASCII_8BIT
|
218
207
|
logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
|
219
|
-
value.encode
|
208
|
+
value = value.encode Encoding::UTF_8
|
220
209
|
end
|
221
210
|
value
|
222
211
|
end
|
@@ -230,6 +219,25 @@ module ActiveRecord
|
|
230
219
|
|
231
220
|
# DATABASE STATEMENTS ======================================
|
232
221
|
|
222
|
+
def explain(arel, binds = [])
|
223
|
+
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
|
224
|
+
ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
|
225
|
+
end
|
226
|
+
|
227
|
+
class ExplainPrettyPrinter
|
228
|
+
# Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
|
229
|
+
# the output of the SQLite shell:
|
230
|
+
#
|
231
|
+
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
|
232
|
+
# 0|1|1|SCAN TABLE posts (~100000 rows)
|
233
|
+
#
|
234
|
+
def pp(result) # :nodoc:
|
235
|
+
result.rows.map do |row|
|
236
|
+
row.join('|')
|
237
|
+
end.join("\n") + "\n"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
233
241
|
def exec_query(sql, name = nil, binds = [])
|
234
242
|
log(sql, name, binds) do
|
235
243
|
|
@@ -303,31 +311,36 @@ module ActiveRecord
|
|
303
311
|
end
|
304
312
|
|
305
313
|
def begin_db_transaction #:nodoc:
|
306
|
-
@connection.transaction
|
314
|
+
log('begin transaction',nil) { @connection.transaction }
|
307
315
|
end
|
308
316
|
|
309
317
|
def commit_db_transaction #:nodoc:
|
310
|
-
@connection.commit
|
318
|
+
log('commit transaction',nil) { @connection.commit }
|
311
319
|
end
|
312
320
|
|
313
321
|
def rollback_db_transaction #:nodoc:
|
314
|
-
@connection.rollback
|
322
|
+
log('rollback transaction',nil) { @connection.rollback }
|
315
323
|
end
|
316
324
|
|
317
325
|
# SCHEMA STATEMENTS ========================================
|
318
326
|
|
319
|
-
def tables(name = 'SCHEMA') #:nodoc:
|
327
|
+
def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
|
320
328
|
sql = <<-SQL
|
321
329
|
SELECT name
|
322
330
|
FROM sqlite_master
|
323
331
|
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
|
324
332
|
SQL
|
333
|
+
sql << " AND name = #{quote_table_name(table_name)}" if table_name
|
325
334
|
|
326
335
|
exec_query(sql, name).map do |row|
|
327
336
|
row['name']
|
328
337
|
end
|
329
338
|
end
|
330
339
|
|
340
|
+
def table_exists?(name)
|
341
|
+
name && tables('SCHEMA', name).any?
|
342
|
+
end
|
343
|
+
|
331
344
|
# Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
|
332
345
|
def columns(table_name, name = nil) #:nodoc:
|
333
346
|
table_structure(table_name).map do |field|
|
@@ -346,12 +359,12 @@ module ActiveRecord
|
|
346
359
|
|
347
360
|
# Returns an array of indexes for the given table.
|
348
361
|
def indexes(table_name, name = nil) #:nodoc:
|
349
|
-
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})",
|
362
|
+
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
|
350
363
|
IndexDefinition.new(
|
351
364
|
table_name,
|
352
365
|
row['name'],
|
353
366
|
row['unique'] != 0,
|
354
|
-
exec_query("PRAGMA index_info('#{row['name']}')").map { |col|
|
367
|
+
exec_query("PRAGMA index_info('#{row['name']}')", 'SCHEMA').map { |col|
|
355
368
|
col['name']
|
356
369
|
})
|
357
370
|
end
|
@@ -393,8 +406,15 @@ module ActiveRecord
|
|
393
406
|
end
|
394
407
|
|
395
408
|
def remove_column(table_name, *column_names) #:nodoc:
|
396
|
-
raise ArgumentError.new("You must specify at least one column name.
|
397
|
-
|
409
|
+
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
|
410
|
+
|
411
|
+
if column_names.flatten!
|
412
|
+
message = 'Passing array to remove_columns is deprecated, please use ' +
|
413
|
+
'multiple arguments, like: `remove_columns(:posts, :foo, :bar)`'
|
414
|
+
ActiveSupport::Deprecation.warn message, caller
|
415
|
+
end
|
416
|
+
|
417
|
+
column_names.each do |column_name|
|
398
418
|
alter_table(table_name) do |definition|
|
399
419
|
definition.columns.delete(definition[column_name])
|
400
420
|
end
|
@@ -425,6 +445,8 @@ module ActiveRecord
|
|
425
445
|
self.limit = options[:limit] if options.include?(:limit)
|
426
446
|
self.default = options[:default] if include_default
|
427
447
|
self.null = options[:null] if options.include?(:null)
|
448
|
+
self.precision = options[:precision] if options.include?(:precision)
|
449
|
+
self.scale = options[:scale] if options.include?(:scale)
|
428
450
|
end
|
429
451
|
end
|
430
452
|
end
|
@@ -468,7 +490,11 @@ module ActiveRecord
|
|
468
490
|
end
|
469
491
|
|
470
492
|
def copy_table(from, to, options = {}) #:nodoc:
|
471
|
-
|
493
|
+
from_primary_key = primary_key(from)
|
494
|
+
options[:primary_key] = from_primary_key if from_primary_key != 'id'
|
495
|
+
unless options[:primary_key]
|
496
|
+
options[:id] = !columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == from_primary_key
|
497
|
+
end
|
472
498
|
create_table(to, options) do |definition|
|
473
499
|
@definition = definition
|
474
500
|
columns(from).each do |column|
|
@@ -479,9 +505,10 @@ module ActiveRecord
|
|
479
505
|
|
480
506
|
@definition.column(column_name, column.type,
|
481
507
|
:limit => column.limit, :default => column.default,
|
508
|
+
:precision => column.precision, :scale => column.scale,
|
482
509
|
:null => column.null)
|
483
510
|
end
|
484
|
-
@definition.primary_key(
|
511
|
+
@definition.primary_key(from_primary_key) if from_primary_key
|
485
512
|
yield @definition if block_given?
|
486
513
|
end
|
487
514
|
|
@@ -507,7 +534,7 @@ module ActiveRecord
|
|
507
534
|
|
508
535
|
unless columns.empty?
|
509
536
|
# index name can't be the same
|
510
|
-
opts = { :name => name.gsub(/_(#{from})_/, "
|
537
|
+
opts = { :name => name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") }
|
511
538
|
opts[:unique] = true if index.unique
|
512
539
|
add_index(to, columns, opts)
|
513
540
|
end
|
@@ -2,7 +2,7 @@ module ActiveRecord
|
|
2
2
|
# = Active Record Counter Cache
|
3
3
|
module CounterCache
|
4
4
|
# Resets one or more counter caches to their correct value using an SQL
|
5
|
-
# count query.
|
5
|
+
# count query. This is useful when adding new counter caches, or if the
|
6
6
|
# counter has been corrupted or modified directly by SQL.
|
7
7
|
#
|
8
8
|
# ==== Parameters
|
@@ -19,15 +19,20 @@ module ActiveRecord
|
|
19
19
|
counters.each do |association|
|
20
20
|
has_many_association = reflect_on_association(association.to_sym)
|
21
21
|
|
22
|
-
|
22
|
+
if has_many_association.options[:as]
|
23
23
|
has_many_association.options[:as].to_s.classify
|
24
24
|
else
|
25
25
|
self.name
|
26
26
|
end
|
27
27
|
|
28
|
+
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
|
29
|
+
has_many_association = has_many_association.through_reflection
|
30
|
+
end
|
31
|
+
|
32
|
+
foreign_key = has_many_association.foreign_key.to_s
|
28
33
|
child_class = has_many_association.klass
|
29
34
|
belongs_to = child_class.reflect_on_all_associations(:belongs_to)
|
30
|
-
reflection = belongs_to.find { |e| e.
|
35
|
+
reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
|
31
36
|
counter_name = reflection.counter_cache_column
|
32
37
|
|
33
38
|
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
|
@@ -65,7 +70,7 @@ module ActiveRecord
|
|
65
70
|
# Post.update_counters [10, 15], :comment_count => 1
|
66
71
|
# # Executes the following SQL:
|
67
72
|
# # UPDATE posts
|
68
|
-
# # SET comment_count = COALESCE(comment_count, 0) + 1
|
73
|
+
# # SET comment_count = COALESCE(comment_count, 0) + 1
|
69
74
|
# # WHERE id IN (10, 15)
|
70
75
|
def update_counters(id, counters)
|
71
76
|
updates = counters.map do |counter_name, value|
|
@@ -18,6 +18,10 @@ module ActiveRecord
|
|
18
18
|
when /^find_by_([_a-zA-Z]\w*)\!$/
|
19
19
|
bang = true
|
20
20
|
names = $1
|
21
|
+
when /^find_or_create_by_([_a-zA-Z]\w*)\!$/
|
22
|
+
bang = true
|
23
|
+
instantiator = :create
|
24
|
+
names = $1
|
21
25
|
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
22
26
|
instantiator = $1 == 'initialize' ? :new : :create
|
23
27
|
names = $2
|
@@ -52,5 +56,13 @@ module ActiveRecord
|
|
52
56
|
def bang?
|
53
57
|
@bang
|
54
58
|
end
|
59
|
+
|
60
|
+
def save_record?
|
61
|
+
@instantiator == :create
|
62
|
+
end
|
63
|
+
|
64
|
+
def save_method
|
65
|
+
bang? ? :save! : :save
|
66
|
+
end
|
55
67
|
end
|
56
68
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module DynamicMatchers
|
3
|
+
def respond_to?(method_id, include_private = false)
|
4
|
+
if match = DynamicFinderMatch.match(method_id)
|
5
|
+
return true if all_attributes_exists?(match.attribute_names)
|
6
|
+
elsif match = DynamicScopeMatch.match(method_id)
|
7
|
+
return true if all_attributes_exists?(match.attribute_names)
|
8
|
+
end
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
|
16
|
+
# <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
|
17
|
+
# section at the top of this file for more detailed information.
|
18
|
+
#
|
19
|
+
# It's even possible to use all the additional parameters to +find+. For example, the
|
20
|
+
# full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
|
21
|
+
#
|
22
|
+
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
|
23
|
+
# is first invoked, so that future attempts to use it do not run through method_missing.
|
24
|
+
def method_missing(method_id, *arguments, &block)
|
25
|
+
if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
|
26
|
+
attribute_names = match.attribute_names
|
27
|
+
super unless all_attributes_exists?(attribute_names)
|
28
|
+
if !(match.is_a?(DynamicFinderMatch) && match.instantiator? && arguments.first.is_a?(Hash)) && arguments.size < attribute_names.size
|
29
|
+
method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
|
30
|
+
backtrace = [method_trace] + caller
|
31
|
+
raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
|
32
|
+
end
|
33
|
+
if match.respond_to?(:scope?) && match.scope?
|
34
|
+
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
35
|
+
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
36
|
+
attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
|
37
|
+
#
|
38
|
+
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
39
|
+
end # end
|
40
|
+
METHOD
|
41
|
+
send(method_id, *arguments)
|
42
|
+
elsif match.finder?
|
43
|
+
options = if arguments.length > attribute_names.size
|
44
|
+
arguments.extract_options!
|
45
|
+
else
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
|
49
|
+
relation = options.any? ? scoped(options) : scoped
|
50
|
+
relation.send :find_by_attributes, match, attribute_names, *arguments, &block
|
51
|
+
elsif match.instantiator?
|
52
|
+
scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
|
53
|
+
end
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
|
60
|
+
def expand_attribute_names_for_aggregates(attribute_names)
|
61
|
+
attribute_names.map { |attribute_name|
|
62
|
+
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
|
63
|
+
aggregate_mapping(aggregation).map do |field_attr, _|
|
64
|
+
field_attr.to_sym
|
65
|
+
end
|
66
|
+
else
|
67
|
+
attribute_name.to_sym
|
68
|
+
end
|
69
|
+
}.flatten
|
70
|
+
end
|
71
|
+
|
72
|
+
def all_attributes_exists?(attribute_names)
|
73
|
+
(expand_attribute_names_for_aggregates(attribute_names) -
|
74
|
+
column_methods_hash.keys).empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
def aggregate_mapping(reflection)
|
78
|
+
mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
|
79
|
+
mapping.first.is_a?(Array) ? mapping : [mapping]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
data/lib/active_record/errors.rb
CHANGED
@@ -87,7 +87,7 @@ module ActiveRecord
|
|
87
87
|
#
|
88
88
|
# For example, in
|
89
89
|
#
|
90
|
-
# Location.
|
90
|
+
# Location.where("lat = ? AND lng = ?", 53.7362)
|
91
91
|
#
|
92
92
|
# two placeholders are given but only one variable to fill them.
|
93
93
|
class PreparedStatementInvalid < ActiveRecordError
|
@@ -99,6 +99,16 @@ module ActiveRecord
|
|
99
99
|
#
|
100
100
|
# Read more about optimistic locking in ActiveRecord::Locking module RDoc.
|
101
101
|
class StaleObjectError < ActiveRecordError
|
102
|
+
attr_reader :record, :attempted_action
|
103
|
+
|
104
|
+
def initialize(record, attempted_action)
|
105
|
+
@record = record
|
106
|
+
@attempted_action = attempted_action
|
107
|
+
end
|
108
|
+
|
109
|
+
def message
|
110
|
+
"Attempted to #{attempted_action} a stale object: #{record.class.name}"
|
111
|
+
end
|
102
112
|
end
|
103
113
|
|
104
114
|
# Raised when association is being configured improperly or
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Explain
|
5
|
+
def self.extended(base)
|
6
|
+
base.class_eval do
|
7
|
+
# If a query takes longer than these many seconds we log its query plan
|
8
|
+
# automatically. nil disables this feature.
|
9
|
+
class_attribute :auto_explain_threshold_in_seconds, :instance_writer => false
|
10
|
+
self.auto_explain_threshold_in_seconds = nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# If the database adapter supports explain and auto explain is enabled,
|
15
|
+
# this method triggers EXPLAIN logging for the queries triggered by the
|
16
|
+
# block if it takes more than the threshold as a whole. That is, the
|
17
|
+
# threshold is not checked against each individual query, but against the
|
18
|
+
# duration of the entire block. This approach is convenient for relations.
|
19
|
+
|
20
|
+
#
|
21
|
+
# The available_queries_for_explain thread variable collects the queries
|
22
|
+
# to be explained. If the value is nil, it means queries are not being
|
23
|
+
# currently collected. A false value indicates collecting is turned
|
24
|
+
# off. Otherwise it is an array of queries.
|
25
|
+
def logging_query_plan # :nodoc:
|
26
|
+
return yield unless logger
|
27
|
+
|
28
|
+
threshold = auto_explain_threshold_in_seconds
|
29
|
+
current = Thread.current
|
30
|
+
if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil?
|
31
|
+
begin
|
32
|
+
queries = current[:available_queries_for_explain] = []
|
33
|
+
start = Time.now
|
34
|
+
result = yield
|
35
|
+
logger.warn(exec_explain(queries)) if Time.now - start > threshold
|
36
|
+
result
|
37
|
+
ensure
|
38
|
+
current[:available_queries_for_explain] = nil
|
39
|
+
end
|
40
|
+
else
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Relation#explain needs to be able to collect the queries regardless of
|
46
|
+
# whether auto explain is enabled. This method serves that purpose.
|
47
|
+
def collecting_queries_for_explain # :nodoc:
|
48
|
+
current = Thread.current
|
49
|
+
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
|
50
|
+
return yield, current[:available_queries_for_explain]
|
51
|
+
ensure
|
52
|
+
# Note that the return value above does not depend on this assigment.
|
53
|
+
current[:available_queries_for_explain] = original
|
54
|
+
end
|
55
|
+
|
56
|
+
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
|
57
|
+
# Returns a formatted string ready to be logged.
|
58
|
+
def exec_explain(queries) # :nodoc:
|
59
|
+
queries && queries.map do |sql, bind|
|
60
|
+
[].tap do |msg|
|
61
|
+
msg << "EXPLAIN for: #{sql}"
|
62
|
+
unless bind.empty?
|
63
|
+
bind_msg = bind.map {|col, val| [col.name, val]}.inspect
|
64
|
+
msg.last << " #{bind_msg}"
|
65
|
+
end
|
66
|
+
msg << connection.explain(sql, bind)
|
67
|
+
end.join("\n")
|
68
|
+
end.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Silences automatic EXPLAIN logging for the duration of the block.
|
72
|
+
#
|
73
|
+
# This has high priority, no EXPLAINs will be run even if downwards
|
74
|
+
# the threshold is set to 0.
|
75
|
+
#
|
76
|
+
# As the name of the method suggests this only applies to automatic
|
77
|
+
# EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
|
78
|
+
def silence_auto_explain
|
79
|
+
current = Thread.current
|
80
|
+
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
|
81
|
+
yield
|
82
|
+
ensure
|
83
|
+
current[:available_queries_for_explain] = original
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_support/notifications'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class ExplainSubscriber # :nodoc:
|
5
|
+
def call(*args)
|
6
|
+
if queries = Thread.current[:available_queries_for_explain]
|
7
|
+
payload = args.last
|
8
|
+
queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
|
13
|
+
# our own EXPLAINs now matter how loopingly beautiful that would be.
|
14
|
+
#
|
15
|
+
# On the other hand, we want to monitor the performance of our real database
|
16
|
+
# queries, not the performance of the access to the query cache.
|
17
|
+
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
|
18
|
+
EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
|
19
|
+
def ignore_payload?(payload)
|
20
|
+
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveSupport::Notifications.subscribe("sql.active_record", new)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
begin
|
2
|
+
require 'psych'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'erb'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
class Fixtures
|
11
|
+
class File
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
##
|
15
|
+
# Open a fixture file named +file+. When called with a block, the block
|
16
|
+
# is called with the filehandle and the filehandle is automatically closed
|
17
|
+
# when the block finishes.
|
18
|
+
def self.open(file)
|
19
|
+
x = new file
|
20
|
+
block_given? ? yield(x) : x
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(file)
|
24
|
+
@file = file
|
25
|
+
@rows = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
rows.each(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
RESCUE_ERRORS = [ ArgumentError ] # :nodoc:
|
33
|
+
|
34
|
+
private
|
35
|
+
if defined?(Psych) && defined?(Psych::SyntaxError)
|
36
|
+
RESCUE_ERRORS << Psych::SyntaxError
|
37
|
+
end
|
38
|
+
|
39
|
+
def rows
|
40
|
+
return @rows if @rows
|
41
|
+
|
42
|
+
begin
|
43
|
+
data = YAML.load(render(IO.read(@file)))
|
44
|
+
rescue *RESCUE_ERRORS => error
|
45
|
+
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
|
46
|
+
end
|
47
|
+
@rows = data ? validate(data).to_a : []
|
48
|
+
end
|
49
|
+
|
50
|
+
def render(content)
|
51
|
+
ERB.new(content).result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Validate our unmarshalled data.
|
55
|
+
def validate(data)
|
56
|
+
unless Hash === data || YAML::Omap === data
|
57
|
+
raise Fixture::FormatError, 'fixture is not a hash'
|
58
|
+
end
|
59
|
+
|
60
|
+
raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
|
61
|
+
data
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|