ocean-dynamo 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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