ocean-dynamo 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,653 @@
1
+ module ActiveRecord
2
+ # = Active Record Relation
3
+ class Relation
4
+ JoinOperation = Struct.new(:relation, :join_class, :on)
5
+
6
+ MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
7
+ :order, :joins, :where, :having, :bind, :references,
8
+ :extending]
9
+
10
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
11
+ :reverse_order, :distinct, :create_with, :uniq]
12
+
13
+ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
14
+
15
+ include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
16
+
17
+ attr_reader :table, :klass, :loaded
18
+ attr_accessor :default_scoped, :proxy_association
19
+ alias :model :klass
20
+ alias :loaded? :loaded
21
+ alias :default_scoped? :default_scoped
22
+
23
+ def initialize(klass, table, values = {})
24
+ @klass = klass
25
+ @table = table
26
+ @values = values
27
+ @implicit_readonly = nil
28
+ @loaded = false
29
+ @default_scoped = false
30
+ end
31
+
32
+ def initialize_copy(other)
33
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
34
+ # https://bugs.ruby-lang.org/issues/7166
35
+ @values = Hash[@values]
36
+ @values[:bind] = @values[:bind].dup if @values.key? :bind
37
+ reset
38
+ end
39
+
40
+ def insert(values)
41
+ primary_key_value = nil
42
+
43
+ if primary_key && Hash === values
44
+ primary_key_value = values[values.keys.find { |k|
45
+ k.name == primary_key
46
+ }]
47
+
48
+ if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
49
+ primary_key_value = connection.next_sequence_value(klass.sequence_name)
50
+ values[klass.arel_table[klass.primary_key]] = primary_key_value
51
+ end
52
+ end
53
+
54
+ im = arel.create_insert
55
+ im.into @table
56
+
57
+ conn = @klass.connection
58
+
59
+ substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
60
+ binds = substitutes.map do |arel_attr, value|
61
+ [@klass.columns_hash[arel_attr.name], value]
62
+ end
63
+
64
+ substitutes.each_with_index do |tuple, i|
65
+ tuple[1] = conn.substitute_at(binds[i][0], i)
66
+ end
67
+
68
+ if values.empty? # empty insert
69
+ im.values = Arel.sql(connection.empty_insert_statement_value)
70
+ else
71
+ im.insert substitutes
72
+ end
73
+
74
+ conn.insert(
75
+ im,
76
+ 'SQL',
77
+ primary_key,
78
+ primary_key_value,
79
+ nil,
80
+ binds)
81
+ end
82
+
83
+ # Initializes new record from relation while maintaining the current
84
+ # scope.
85
+ #
86
+ # Expects arguments in the same format as +Base.new+.
87
+ #
88
+ # users = User.where(name: 'DHH')
89
+ # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
90
+ #
91
+ # You can also pass a block to new with the new record as argument:
92
+ #
93
+ # user = users.new { |user| user.name = 'Oscar' }
94
+ # user.name # => Oscar
95
+ def new(*args, &block)
96
+ scoping { @klass.new(*args, &block) }
97
+ end
98
+
99
+ alias build new
100
+
101
+ # Tries to create a new record with the same scoped attributes
102
+ # defined in the relation. Returns the initialized object if validation fails.
103
+ #
104
+ # Expects arguments in the same format as +Base.create+.
105
+ #
106
+ # ==== Examples
107
+ # users = User.where(name: 'Oscar')
108
+ # users.create # #<User id: 3, name: "oscar", ...>
109
+ #
110
+ # users.create(name: 'fxn')
111
+ # users.create # #<User id: 4, name: "fxn", ...>
112
+ #
113
+ # users.create { |user| user.name = 'tenderlove' }
114
+ # # #<User id: 5, name: "tenderlove", ...>
115
+ #
116
+ # users.create(name: nil) # validation on name
117
+ # # #<User id: nil, name: nil, ...>
118
+ def create(*args, &block)
119
+ scoping { @klass.create(*args, &block) }
120
+ end
121
+
122
+ # Similar to #create, but calls +create!+ on the base class. Raises
123
+ # an exception if a validation error occurs.
124
+ #
125
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
126
+ def create!(*args, &block)
127
+ scoping { @klass.create!(*args, &block) }
128
+ end
129
+
130
+ def first_or_create(attributes = nil, &block) # :nodoc:
131
+ first || create(attributes, &block)
132
+ end
133
+
134
+ def first_or_create!(attributes = nil, &block) # :nodoc:
135
+ first || create!(attributes, &block)
136
+ end
137
+
138
+ def first_or_initialize(attributes = nil, &block) # :nodoc:
139
+ first || new(attributes, &block)
140
+ end
141
+
142
+ # Finds the first record with the given attributes, or creates a record
143
+ # with the attributes if one is not found:
144
+ #
145
+ # # Find the first user named "Penélope" or create a new one.
146
+ # User.find_or_create_by(first_name: 'Penélope')
147
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
148
+ #
149
+ # # Find the first user named "Penélope" or create a new one.
150
+ # # We already have one so the existing record will be returned.
151
+ # User.find_or_create_by(first_name: 'Penélope')
152
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
153
+ #
154
+ # # Find the first user named "Scarlett" or create a new one with
155
+ # # a particular last name.
156
+ # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
157
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
158
+ #
159
+ # This method accepts a block, which is passed down to +create+. The last example
160
+ # above can be alternatively written this way:
161
+ #
162
+ # # Find the first user named "Scarlett" or create a new one with a
163
+ # # different last name.
164
+ # User.find_or_create_by(first_name: 'Scarlett') do |user|
165
+ # user.last_name = 'Johansson'
166
+ # end
167
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
168
+ #
169
+ # This method always returns a record, but if creation was attempted and
170
+ # failed due to validation errors it won't be persisted, you get what
171
+ # +create+ returns in such situation.
172
+ #
173
+ # Please note *this method is not atomic*, it runs first a SELECT, and if
174
+ # there are no results an INSERT is attempted. If there are other threads
175
+ # or processes there is a race condition between both calls and it could
176
+ # be the case that you end up with two similar records.
177
+ #
178
+ # Whether that is a problem or not depends on the logic of the
179
+ # application, but in the particular case in which rows have a UNIQUE
180
+ # constraint an exception may be raised, just retry:
181
+ #
182
+ # begin
183
+ # CreditAccount.find_or_create_by(user_id: user.id)
184
+ # rescue ActiveRecord::RecordNotUnique
185
+ # retry
186
+ # end
187
+ #
188
+ def find_or_create_by(attributes, &block)
189
+ find_by(attributes) || create(attributes, &block)
190
+ end
191
+
192
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
193
+ # is raised if the created record is invalid.
194
+ def find_or_create_by!(attributes, &block)
195
+ find_by(attributes) || create!(attributes, &block)
196
+ end
197
+
198
+ # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
199
+ def find_or_initialize_by(attributes, &block)
200
+ find_by(attributes) || new(attributes, &block)
201
+ end
202
+
203
+ # Runs EXPLAIN on the query or queries triggered by this relation and
204
+ # returns the result as a string. The string is formatted imitating the
205
+ # ones printed by the database shell.
206
+ #
207
+ # Note that this method actually runs the queries, since the results of some
208
+ # are needed by the next ones when eager loading is going on.
209
+ #
210
+ # Please see further details in the
211
+ # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
212
+ def explain
213
+ exec_explain(collecting_queries_for_explain { exec_queries })
214
+ end
215
+
216
+ # Converts relation objects to Array.
217
+ def to_a
218
+ load
219
+ @records
220
+ end
221
+
222
+ def as_json(options = nil) #:nodoc:
223
+ to_a.as_json(options)
224
+ end
225
+
226
+ # Returns size of the records.
227
+ def size
228
+ loaded? ? @records.length : count
229
+ end
230
+
231
+ # Returns true if there are no records.
232
+ def empty?
233
+ return @records.empty? if loaded?
234
+
235
+ c = count
236
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
237
+ end
238
+
239
+ # Returns true if there are any records.
240
+ def any?
241
+ if block_given?
242
+ to_a.any? { |*block_args| yield(*block_args) }
243
+ else
244
+ !empty?
245
+ end
246
+ end
247
+
248
+ # Returns true if there is more than one record.
249
+ def many?
250
+ if block_given?
251
+ to_a.many? { |*block_args| yield(*block_args) }
252
+ else
253
+ limit_value ? to_a.many? : size > 1
254
+ end
255
+ end
256
+
257
+ # Scope all queries to the current scope.
258
+ #
259
+ # Comment.where(post_id: 1).scoping do
260
+ # Comment.first
261
+ # end
262
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
263
+ #
264
+ # Please check unscoped if you want to remove all previous scopes (including
265
+ # the default_scope) during the execution of a block.
266
+ def scoping
267
+ previous, klass.current_scope = klass.current_scope, self
268
+ yield
269
+ ensure
270
+ klass.current_scope = previous
271
+ end
272
+
273
+ # Updates all records with details given if they match a set of conditions supplied, limits and order can
274
+ # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
275
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
276
+ # or validations.
277
+ #
278
+ # ==== Parameters
279
+ #
280
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
281
+ #
282
+ # ==== Examples
283
+ #
284
+ # # Update all customers with the given attributes
285
+ # Customer.update_all wants_email: true
286
+ #
287
+ # # Update all books with 'Rails' in their title
288
+ # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
289
+ #
290
+ # # Update all books that match conditions, but limit it to 5 ordered by date
291
+ # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
292
+ def update_all(updates)
293
+ raise ArgumentError, "Empty list of attributes to change" if updates.blank?
294
+
295
+ stmt = Arel::UpdateManager.new(arel.engine)
296
+
297
+ stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
298
+ stmt.table(table)
299
+ stmt.key = table[primary_key]
300
+
301
+ if with_default_scope.joins_values.any?
302
+ @klass.connection.join_to_update(stmt, arel)
303
+ else
304
+ stmt.take(arel.limit)
305
+ stmt.order(*arel.orders)
306
+ stmt.wheres = arel.constraints
307
+ end
308
+
309
+ @klass.connection.update stmt, 'SQL', bind_values
310
+ end
311
+
312
+ # Updates an object (or multiple objects) and saves it to the database, if validations pass.
313
+ # The resulting object is returned whether the object was saved successfully to the database or not.
314
+ #
315
+ # ==== Parameters
316
+ #
317
+ # * +id+ - This should be the id or an array of ids to be updated.
318
+ # * +attributes+ - This should be a hash of attributes or an array of hashes.
319
+ #
320
+ # ==== Examples
321
+ #
322
+ # # Updates one record
323
+ # Person.update(15, user_name: 'Samuel', group: 'expert')
324
+ #
325
+ # # Updates multiple records
326
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
327
+ # Person.update(people.keys, people.values)
328
+ def update(id, attributes)
329
+ if id.is_a?(Array)
330
+ id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
331
+ else
332
+ object = find(id)
333
+ object.update(attributes)
334
+ object
335
+ end
336
+ end
337
+
338
+ # Destroys the records matching +conditions+ by instantiating each
339
+ # record and calling its +destroy+ method. Each object's callbacks are
340
+ # executed (including <tt>:dependent</tt> association options). Returns the
341
+ # collection of objects that were destroyed; each will be frozen, to
342
+ # reflect that no changes should be made (since they can't be persisted).
343
+ #
344
+ # Note: Instantiation, callback execution, and deletion of each
345
+ # record can be time consuming when you're removing many records at
346
+ # once. It generates at least one SQL +DELETE+ query per record (or
347
+ # possibly more, to enforce your callbacks). If you want to delete many
348
+ # rows quickly, without concern for their associations or callbacks, use
349
+ # +delete_all+ instead.
350
+ #
351
+ # ==== Parameters
352
+ #
353
+ # * +conditions+ - A string, array, or hash that specifies which records
354
+ # to destroy. If omitted, all records are destroyed. See the
355
+ # Conditions section in the introduction to ActiveRecord::Base for
356
+ # more information.
357
+ #
358
+ # ==== Examples
359
+ #
360
+ # Person.destroy_all("last_login < '2004-04-04'")
361
+ # Person.destroy_all(status: "inactive")
362
+ # Person.where(age: 0..18).destroy_all
363
+ def destroy_all(conditions = nil)
364
+ if conditions
365
+ where(conditions).destroy_all
366
+ else
367
+ to_a.each {|object| object.destroy }.tap { reset }
368
+ end
369
+ end
370
+
371
+ # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
372
+ # therefore all callbacks and filters are fired off before the object is deleted. This method is
373
+ # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
374
+ #
375
+ # This essentially finds the object (or multiple objects) with the given id, creates a new object
376
+ # from the attributes, and then calls destroy on it.
377
+ #
378
+ # ==== Parameters
379
+ #
380
+ # * +id+ - Can be either an Integer or an Array of Integers.
381
+ #
382
+ # ==== Examples
383
+ #
384
+ # # Destroy a single object
385
+ # Todo.destroy(1)
386
+ #
387
+ # # Destroy multiple objects
388
+ # todos = [1,2,3]
389
+ # Todo.destroy(todos)
390
+ def destroy(id)
391
+ if id.is_a?(Array)
392
+ id.map { |one_id| destroy(one_id) }
393
+ else
394
+ find(id).destroy
395
+ end
396
+ end
397
+
398
+ # Deletes the records matching +conditions+ without instantiating the records
399
+ # first, and hence not calling the +destroy+ method nor invoking callbacks. This
400
+ # is a single SQL DELETE statement that goes straight to the database, much more
401
+ # efficient than +destroy_all+. Be careful with relations though, in particular
402
+ # <tt>:dependent</tt> rules defined on associations are not honored. Returns the
403
+ # number of rows affected.
404
+ #
405
+ # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
406
+ # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
407
+ # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
408
+ #
409
+ # Both calls delete the affected posts all at once with a single DELETE statement.
410
+ # If you need to destroy dependent associations or call your <tt>before_*</tt> or
411
+ # +after_destroy+ callbacks, use the +destroy_all+ method instead.
412
+ #
413
+ # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
414
+ #
415
+ # Post.limit(100).delete_all
416
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
417
+ def delete_all(conditions = nil)
418
+ raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
419
+
420
+ if conditions
421
+ where(conditions).delete_all
422
+ else
423
+ stmt = Arel::DeleteManager.new(arel.engine)
424
+ stmt.from(table)
425
+
426
+ if with_default_scope.joins_values.any?
427
+ @klass.connection.join_to_delete(stmt, arel, table[primary_key])
428
+ else
429
+ stmt.wheres = arel.constraints
430
+ end
431
+
432
+ affected = @klass.connection.delete(stmt, 'SQL', bind_values)
433
+
434
+ reset
435
+ affected
436
+ end
437
+ end
438
+
439
+ # Deletes the row with a primary key matching the +id+ argument, using a
440
+ # SQL +DELETE+ statement, and returns the number of rows deleted. Active
441
+ # Record objects are not instantiated, so the object's callbacks are not
442
+ # executed, including any <tt>:dependent</tt> association options.
443
+ #
444
+ # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
445
+ #
446
+ # Note: Although it is often much faster than the alternative,
447
+ # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
448
+ # your application that ensures referential integrity or performs other
449
+ # essential jobs.
450
+ #
451
+ # ==== Examples
452
+ #
453
+ # # Delete a single row
454
+ # Todo.delete(1)
455
+ #
456
+ # # Delete multiple rows
457
+ # Todo.delete([2,3,4])
458
+ def delete(id_or_array)
459
+ where(primary_key => id_or_array).delete_all
460
+ end
461
+
462
+ # Causes the records to be loaded from the database if they have not
463
+ # been loaded already. You can use this if for some reason you need
464
+ # to explicitly load some records before actually using them. The
465
+ # return value is the relation itself, not the records.
466
+ #
467
+ # Post.where(published: true).load # => #<ActiveRecord::Relation>
468
+ def load
469
+ exec_queries unless loaded?
470
+
471
+ self
472
+ end
473
+
474
+ # Forces reloading of relation.
475
+ def reload
476
+ reset
477
+ load
478
+ end
479
+
480
+ def reset
481
+ @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
482
+ @should_eager_load = @join_dependency = nil
483
+ @records = []
484
+ self
485
+ end
486
+
487
+ # Returns sql statement for the relation.
488
+ #
489
+ # User.where(name: 'Oscar').to_sql
490
+ # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
491
+ def to_sql
492
+ @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
493
+ end
494
+
495
+ # Returns a hash of where conditions.
496
+ #
497
+ # User.where(name: 'Oscar').where_values_hash
498
+ # # => {name: "Oscar"}
499
+ def where_values_hash
500
+ equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
501
+ node.left.relation.name == table_name
502
+ }
503
+
504
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
505
+
506
+ Hash[equalities.map { |where|
507
+ name = where.left.name
508
+ [name, binds.fetch(name.to_s) { where.right }]
509
+ }]
510
+ end
511
+
512
+ def scope_for_create
513
+ @scope_for_create ||= where_values_hash.merge(create_with_value)
514
+ end
515
+
516
+ # Returns true if relation needs eager loading.
517
+ def eager_loading?
518
+ @should_eager_load ||=
519
+ eager_load_values.any? ||
520
+ includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
521
+ end
522
+
523
+ # Joins that are also marked for preloading. In which case we should just eager load them.
524
+ # Note that this is a naive implementation because we could have strings and symbols which
525
+ # represent the same association, but that aren't matched by this. Also, we could have
526
+ # nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
527
+ def joined_includes_values
528
+ includes_values & joins_values
529
+ end
530
+
531
+ # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
532
+ # to maintain backwards compatibility. Use +distinct_value+ instead.
533
+ def uniq_value
534
+ distinct_value
535
+ end
536
+
537
+ # Compares two relations for equality.
538
+ def ==(other)
539
+ case other
540
+ when Relation
541
+ other.to_sql == to_sql
542
+ when Array
543
+ to_a == other
544
+ end
545
+ end
546
+
547
+ def pretty_print(q)
548
+ q.pp(self.to_a)
549
+ end
550
+
551
+ def with_default_scope #:nodoc:
552
+ if default_scoped? && default_scope = klass.send(:build_default_scope)
553
+ default_scope = default_scope.merge(self)
554
+ default_scope.default_scoped = false
555
+ default_scope
556
+ else
557
+ self
558
+ end
559
+ end
560
+
561
+ # Returns true if relation is blank.
562
+ def blank?
563
+ to_a.blank?
564
+ end
565
+
566
+ def values
567
+ Hash[@values]
568
+ end
569
+
570
+ def inspect
571
+ entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect)
572
+ entries[10] = '...' if entries.size == 11
573
+
574
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
575
+ end
576
+
577
+ private
578
+
579
+ def exec_queries
580
+ default_scoped = with_default_scope
581
+
582
+ if default_scoped.equal?(self)
583
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
584
+
585
+ preload = preload_values
586
+ preload += includes_values unless eager_loading?
587
+ preload.each do |associations|
588
+ ActiveRecord::Associations::Preloader.new(@records, associations).run
589
+ end
590
+
591
+ # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
592
+ # are JOINS and no explicit SELECT.
593
+ readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
594
+ @records.each { |record| record.readonly! } if readonly
595
+ else
596
+ @records = default_scoped.to_a
597
+ end
598
+
599
+ @loaded = true
600
+ @records
601
+ end
602
+
603
+ def references_eager_loaded_tables?
604
+ joined_tables = arel.join_sources.map do |join|
605
+ if join.is_a?(Arel::Nodes::StringJoin)
606
+ tables_in_string(join.left)
607
+ else
608
+ [join.left.table_name, join.left.table_alias]
609
+ end
610
+ end
611
+
612
+ joined_tables += [table.name, table.table_alias]
613
+
614
+ # always convert table names to downcase as in Oracle quoted table names are in uppercase
615
+ joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
616
+ string_tables = tables_in_string(to_sql)
617
+
618
+ if (references_values - joined_tables).any?
619
+ true
620
+ elsif !ActiveRecord::Base.disable_implicit_join_references &&
621
+ (string_tables - joined_tables).any?
622
+ ActiveSupport::Deprecation.warn(
623
+ "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
624
+ "that are referenced in a string SQL snippet. For example: \n" \
625
+ "\n" \
626
+ " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
627
+ "\n" \
628
+ "Currently, Active Record recognizes the table in the string, and knows to JOIN the " \
629
+ "comments table to the query, rather than loading comments in a separate query. " \
630
+ "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
631
+ "Since we don't want to write an SQL parser, we are removing this functionality. " \
632
+ "From now on, you must explicitly tell Active Record when you are referencing a table " \
633
+ "from a string:\n" \
634
+ "\n" \
635
+ " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \
636
+ "\n" \
637
+ "If you don't rely on implicit join references you can disable the feature entirely " \
638
+ "by setting `config.active_record.disable_implicit_join_references = true`."
639
+ )
640
+ true
641
+ else
642
+ false
643
+ end
644
+ end
645
+
646
+ def tables_in_string(string)
647
+ return [] if string.blank?
648
+ # always convert table names to downcase as in Oracle quoted table names are in uppercase
649
+ # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
650
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
651
+ end
652
+ end
653
+ end