orfeas_lyra 0.6.0

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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +222 -0
  3. data/LICENSE +21 -0
  4. data/README.md +1165 -0
  5. data/Rakefile +728 -0
  6. data/app/controllers/lyra/application_controller.rb +23 -0
  7. data/app/controllers/lyra/dashboard_controller.rb +624 -0
  8. data/app/controllers/lyra/flow_controller.rb +224 -0
  9. data/app/controllers/lyra/privacy_controller.rb +182 -0
  10. data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
  11. data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
  12. data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
  13. data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
  14. data/app/views/lyra/dashboard/index.html.erb +119 -0
  15. data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
  16. data/app/views/lyra/dashboard/projections.html.erb +302 -0
  17. data/app/views/lyra/dashboard/schema.html.erb +283 -0
  18. data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
  19. data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
  20. data/app/views/lyra/dashboard/verification.html.erb +370 -0
  21. data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
  22. data/app/views/lyra/flow/timeline.html.erb +260 -0
  23. data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
  24. data/app/views/lyra/privacy/policy.html.erb +188 -0
  25. data/app/workflows/es_async_mode_workflow.rb +80 -0
  26. data/app/workflows/es_sync_mode_workflow.rb +64 -0
  27. data/app/workflows/hijack_mode_workflow.rb +54 -0
  28. data/app/workflows/lifecycle_workflow.rb +43 -0
  29. data/app/workflows/monitor_mode_workflow.rb +39 -0
  30. data/config/privacy_policies.rb +273 -0
  31. data/config/routes.rb +48 -0
  32. data/lib/lyra/aggregate.rb +131 -0
  33. data/lib/lyra/associations/event_aware.rb +225 -0
  34. data/lib/lyra/command.rb +81 -0
  35. data/lib/lyra/command_handler.rb +155 -0
  36. data/lib/lyra/configuration.rb +124 -0
  37. data/lib/lyra/consistency/read_your_writes.rb +91 -0
  38. data/lib/lyra/correlation.rb +144 -0
  39. data/lib/lyra/dual_view.rb +231 -0
  40. data/lib/lyra/engine.rb +67 -0
  41. data/lib/lyra/event.rb +71 -0
  42. data/lib/lyra/event_analyzer.rb +135 -0
  43. data/lib/lyra/event_flow.rb +449 -0
  44. data/lib/lyra/event_mapper.rb +106 -0
  45. data/lib/lyra/event_store_adapter.rb +72 -0
  46. data/lib/lyra/id_generator.rb +137 -0
  47. data/lib/lyra/interceptors/association_interceptor.rb +169 -0
  48. data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
  49. data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
  50. data/lib/lyra/privacy/pii_detector.rb +85 -0
  51. data/lib/lyra/privacy/pii_masker.rb +66 -0
  52. data/lib/lyra/privacy/policy_integration.rb +253 -0
  53. data/lib/lyra/projection.rb +94 -0
  54. data/lib/lyra/projections/async_projection_job.rb +63 -0
  55. data/lib/lyra/projections/cached_projection.rb +322 -0
  56. data/lib/lyra/projections/cached_relation.rb +757 -0
  57. data/lib/lyra/projections/event_store_reader.rb +127 -0
  58. data/lib/lyra/projections/model_projection.rb +143 -0
  59. data/lib/lyra/schema/diff.rb +331 -0
  60. data/lib/lyra/schema/event_class_registrar.rb +63 -0
  61. data/lib/lyra/schema/generator.rb +190 -0
  62. data/lib/lyra/schema/reporter.rb +188 -0
  63. data/lib/lyra/schema/store.rb +156 -0
  64. data/lib/lyra/schema/validator.rb +100 -0
  65. data/lib/lyra/strict_data_access.rb +363 -0
  66. data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
  67. data/lib/lyra/verification/workflow_generator.rb +540 -0
  68. data/lib/lyra/version.rb +3 -0
  69. data/lib/lyra/visualization/activity_heatmap.rb +215 -0
  70. data/lib/lyra/visualization/event_graph.rb +310 -0
  71. data/lib/lyra/visualization/timeline.rb +398 -0
  72. data/lib/lyra.rb +150 -0
  73. data/lib/tasks/dist.rake +391 -0
  74. data/lib/tasks/gems.rake +185 -0
  75. data/lib/tasks/lyra_schema.rake +231 -0
  76. data/lib/tasks/lyra_workflows.rake +452 -0
  77. data/lib/tasks/public_release.rake +351 -0
  78. data/lib/tasks/stats.rake +175 -0
  79. data/lib/tasks/testbed.rake +479 -0
  80. data/lib/tasks/version.rake +159 -0
  81. metadata +221 -0
@@ -0,0 +1,757 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lyra
4
+ module Projections
5
+ # ActiveRecord::Relation-like wrapper for cached projection results.
6
+ #
7
+ # Enables method chaining on cached results so that code written for
8
+ # ActiveRecord works transparently in disabled projections mode.
9
+ #
10
+ # Usage:
11
+ # relation = CachedRelation.new(User, records)
12
+ # relation.where(status: "active").order(:name).limit(10)
13
+ #
14
+ class CachedRelation
15
+ include Enumerable
16
+
17
+ attr_reader :model_class, :records
18
+
19
+ def initialize(model_class, records = [])
20
+ @model_class = model_class
21
+ @records = records.to_a
22
+ end
23
+
24
+ # =========================================================================
25
+ # Enumerable / Array-like interface
26
+ # =========================================================================
27
+
28
+ def each(&block)
29
+ @records.each(&block)
30
+ end
31
+
32
+ def to_a
33
+ @records.dup
34
+ end
35
+
36
+ def to_ary
37
+ to_a
38
+ end
39
+
40
+ def [](index)
41
+ @records[index]
42
+ end
43
+
44
+ def size
45
+ @records.size
46
+ end
47
+
48
+ def length
49
+ size
50
+ end
51
+
52
+ def count(column_name = nil, &block)
53
+ if block_given?
54
+ @records.count(&block)
55
+ elsif column_name
56
+ @records.count { |r| r.send(column_name).present? }
57
+ else
58
+ @records.size
59
+ end
60
+ end
61
+
62
+ def empty?
63
+ @records.empty?
64
+ end
65
+
66
+ def any?(&block)
67
+ block_given? ? @records.any?(&block) : @records.any?
68
+ end
69
+
70
+ def none?(&block)
71
+ block_given? ? @records.none?(&block) : @records.none?
72
+ end
73
+
74
+ def one?(&block)
75
+ block_given? ? @records.one?(&block) : @records.one?
76
+ end
77
+
78
+ def many?
79
+ @records.size > 1
80
+ end
81
+
82
+ def present?
83
+ @records.present?
84
+ end
85
+
86
+ def blank?
87
+ @records.blank?
88
+ end
89
+
90
+ # =========================================================================
91
+ # Finder methods
92
+ # =========================================================================
93
+
94
+ def first(limit = nil)
95
+ limit ? @records.first(limit) : @records.first
96
+ end
97
+
98
+ def last(limit = nil)
99
+ limit ? @records.last(limit) : @records.last
100
+ end
101
+
102
+ def second
103
+ @records[1]
104
+ end
105
+
106
+ def third
107
+ @records[2]
108
+ end
109
+
110
+ def take(limit = nil)
111
+ limit ? @records.first(limit) : @records.first
112
+ end
113
+
114
+ def take!
115
+ take || raise(ActiveRecord::RecordNotFound.new("Couldn't find #{model_class.name}", model_class))
116
+ end
117
+
118
+ def first!
119
+ first || raise(ActiveRecord::RecordNotFound.new("Couldn't find #{model_class.name}", model_class))
120
+ end
121
+
122
+ def last!
123
+ last || raise(ActiveRecord::RecordNotFound.new("Couldn't find #{model_class.name}", model_class))
124
+ end
125
+
126
+ def find(id)
127
+ record = @records.find { |r| r.id.to_s == id.to_s }
128
+ record || raise(ActiveRecord::RecordNotFound.new(
129
+ "Couldn't find #{model_class.name} with '#{model_class.primary_key}'=#{id}",
130
+ model_class, model_class.primary_key, id
131
+ ))
132
+ end
133
+
134
+ def find_by(attributes)
135
+ @records.find do |record|
136
+ attributes.all? { |key, value| matches_value?(record, key, value) }
137
+ end
138
+ end
139
+
140
+ def find_by!(attributes)
141
+ find_by(attributes) || raise(ActiveRecord::RecordNotFound.new(
142
+ "Couldn't find #{model_class.name}", model_class
143
+ ))
144
+ end
145
+
146
+ def exists?(conditions = nil)
147
+ case conditions
148
+ when nil, false
149
+ @records.any?
150
+ when Integer, String
151
+ @records.any? { |r| r.id.to_s == conditions.to_s }
152
+ when Hash
153
+ find_by(conditions).present?
154
+ else
155
+ @records.any?
156
+ end
157
+ end
158
+
159
+ # =========================================================================
160
+ # Query methods (return new CachedRelation for chaining)
161
+ # =========================================================================
162
+
163
+ def where(conditions = nil, *args)
164
+ return self if conditions.nil?
165
+
166
+ # Handle where.not(...) chain
167
+ return WhereChain.new(self) if conditions == :chain
168
+
169
+ filtered = @records.select do |record|
170
+ case conditions
171
+ when Hash
172
+ conditions.all? { |key, value| matches_value?(record, key, value) }
173
+ when String
174
+ # SQL string conditions - can't evaluate, return all
175
+ # This is a limitation of the cached approach
176
+ true
177
+ else
178
+ true
179
+ end
180
+ end
181
+
182
+ self.class.new(model_class, filtered)
183
+ end
184
+
185
+ def not(conditions)
186
+ filtered = @records.reject do |record|
187
+ conditions.all? { |key, value| matches_value?(record, key, value) }
188
+ end
189
+ self.class.new(model_class, filtered)
190
+ end
191
+
192
+ def order(*args)
193
+ return self if args.empty?
194
+
195
+ sorted = @records.sort do |a, b|
196
+ compare_for_order(a, b, args)
197
+ end
198
+
199
+ self.class.new(model_class, sorted)
200
+ end
201
+
202
+ def reorder(*args)
203
+ order(*args)
204
+ end
205
+
206
+ def reverse_order
207
+ self.class.new(model_class, @records.reverse)
208
+ end
209
+
210
+ def limit(count)
211
+ self.class.new(model_class, @records.first(count))
212
+ end
213
+
214
+ def offset(count)
215
+ self.class.new(model_class, @records.drop(count))
216
+ end
217
+
218
+ # =========================================================================
219
+ # Pagination support (Kaminari/WillPaginate compatibility)
220
+ # =========================================================================
221
+
222
+ def page(num)
223
+ @current_page = [num.to_i, 1].max
224
+ @per_page ||= 25
225
+ self
226
+ end
227
+
228
+ def per(num)
229
+ @per_page = num.to_i
230
+ paginated_records
231
+ end
232
+
233
+ def total_pages
234
+ return 1 if @per_page.nil? || @per_page <= 0
235
+ (@records.size.to_f / @per_page).ceil
236
+ end
237
+
238
+ def current_page
239
+ @current_page || 1
240
+ end
241
+
242
+ def total_count
243
+ @records.size
244
+ end
245
+
246
+ def limit_value
247
+ @per_page
248
+ end
249
+
250
+ def offset_value
251
+ return 0 unless @current_page && @per_page
252
+ (@current_page - 1) * @per_page
253
+ end
254
+
255
+ private def paginated_records
256
+ return self unless @current_page && @per_page
257
+
258
+ start_idx = (@current_page - 1) * @per_page
259
+ paginated = @records[start_idx, @per_page] || []
260
+
261
+ result = self.class.new(model_class, paginated)
262
+ result.instance_variable_set(:@current_page, @current_page)
263
+ result.instance_variable_set(:@per_page, @per_page)
264
+ result.instance_variable_set(:@total_records, @records.size)
265
+ result
266
+ end
267
+
268
+ def distinct
269
+ self.class.new(model_class, @records.uniq)
270
+ end
271
+
272
+ def uniq
273
+ distinct
274
+ end
275
+
276
+ # =========================================================================
277
+ # Eager loading (no-ops for cached records - data is already in memory)
278
+ # =========================================================================
279
+
280
+ def preload(*args)
281
+ # Associations are already loaded or don't exist in cache
282
+ # This is a no-op but allows the chain to continue
283
+ self
284
+ end
285
+
286
+ def includes(*args)
287
+ # Same as preload - no-op for cached records
288
+ self
289
+ end
290
+
291
+ def eager_load(*args)
292
+ # Same as preload - no-op for cached records
293
+ self
294
+ end
295
+
296
+ def references(*args)
297
+ self
298
+ end
299
+
300
+ def joins(*args)
301
+ # Can't actually join - return self to allow chain to continue
302
+ # Note: This may produce incorrect results for complex queries
303
+ self
304
+ end
305
+
306
+ def left_joins(*args)
307
+ self
308
+ end
309
+
310
+ def left_outer_joins(*args)
311
+ self
312
+ end
313
+
314
+ # =========================================================================
315
+ # Scoping methods
316
+ # =========================================================================
317
+
318
+ def all
319
+ self
320
+ end
321
+
322
+ def none
323
+ self.class.new(model_class, [])
324
+ end
325
+
326
+ def unscoped
327
+ self
328
+ end
329
+
330
+ def readonly(value = true)
331
+ self
332
+ end
333
+
334
+ # =========================================================================
335
+ # AR internal methods for association scope building
336
+ # These are needed for belongs_to/has_many association loading
337
+ # =========================================================================
338
+
339
+ def alias_tracker
340
+ @alias_tracker ||= ActiveRecord::Associations::AliasTracker.create(
341
+ model_class.connection_pool, table.name, []
342
+ )
343
+ end
344
+
345
+ def table
346
+ model_class.arel_table
347
+ end
348
+
349
+ def connection
350
+ model_class.connection
351
+ end
352
+
353
+ def joins_values
354
+ []
355
+ end
356
+
357
+ def left_outer_joins_values
358
+ []
359
+ end
360
+
361
+ def where_clause
362
+ ActiveRecord::Relation::WhereClause.empty
363
+ end
364
+
365
+ def klass
366
+ model_class
367
+ end
368
+
369
+ def scope_for_create
370
+ {}
371
+ end
372
+
373
+ def limit!(value)
374
+ self.class.new(model_class, @records.first(value))
375
+ end
376
+
377
+ def values
378
+ {}
379
+ end
380
+
381
+ def extending!(*modules, &block)
382
+ # No-op for cached relation - extensions are for AR scopes
383
+ self
384
+ end
385
+
386
+ def extending(*modules, &block)
387
+ self
388
+ end
389
+
390
+ def spawn
391
+ self.class.new(model_class, @records.dup)
392
+ end
393
+
394
+ def merge(other, *rest)
395
+ # For association scopes, just return self since we already have filtered records
396
+ self
397
+ end
398
+
399
+ def merge!(other, *rest)
400
+ self
401
+ end
402
+
403
+ def bind_attribute(name, value)
404
+ self
405
+ end
406
+
407
+ def rewhere(conditions)
408
+ where(conditions)
409
+ end
410
+
411
+ def except(*skips)
412
+ self
413
+ end
414
+
415
+ def only(*keeps)
416
+ self
417
+ end
418
+
419
+ def where!(conditions)
420
+ # In-place where modification (for AR internal use)
421
+ # Filter records and update @records directly
422
+ return self if conditions.nil?
423
+
424
+ @records = @records.select do |record|
425
+ case conditions
426
+ when Hash
427
+ conditions.all? { |key, value| matches_value?(record, key, value) }
428
+ else
429
+ true
430
+ end
431
+ end
432
+ self
433
+ end
434
+
435
+ def order!(*args)
436
+ # In-place order modification
437
+ return self if args.empty?
438
+ @records = @records.sort { |a, b| compare_for_order(a, b, args) }
439
+ self
440
+ end
441
+
442
+ def reselect(*args)
443
+ self
444
+ end
445
+
446
+ def select(*args, &block)
447
+ if block_given?
448
+ # Enumerable select
449
+ self.class.new(model_class, @records.select(&block))
450
+ else
451
+ # AR select (column selection) - return self since we have full records
452
+ self
453
+ end
454
+ end
455
+
456
+ # =========================================================================
457
+ # Aggregations
458
+ # =========================================================================
459
+
460
+ def sum(column_name = nil, &block)
461
+ if block_given?
462
+ @records.sum(&block)
463
+ elsif column_name
464
+ @records.sum { |r| r.send(column_name).to_f }
465
+ else
466
+ 0
467
+ end
468
+ end
469
+
470
+ def average(column_name)
471
+ values = @records.map { |r| r.send(column_name) }.compact
472
+ return nil if values.empty?
473
+ values.sum.to_f / values.size
474
+ end
475
+
476
+ def minimum(column_name)
477
+ @records.map { |r| r.send(column_name) }.compact.min
478
+ end
479
+
480
+ def maximum(column_name)
481
+ @records.map { |r| r.send(column_name) }.compact.max
482
+ end
483
+
484
+ def pluck(*column_names)
485
+ @records.map do |record|
486
+ if column_names.size == 1
487
+ record.send(column_names.first)
488
+ else
489
+ column_names.map { |col| record.send(col) }
490
+ end
491
+ end
492
+ end
493
+
494
+ def ids
495
+ pluck(:id)
496
+ end
497
+
498
+ def pick(*column_names)
499
+ record = first
500
+ return nil unless record
501
+
502
+ if column_names.size == 1
503
+ record.send(column_names.first)
504
+ else
505
+ column_names.map { |col| record.send(col) }
506
+ end
507
+ end
508
+
509
+ # =========================================================================
510
+ # Batching (simplified - all records are in memory)
511
+ # =========================================================================
512
+
513
+ def find_each(batch_size: 1000, &block)
514
+ each(&block)
515
+ end
516
+
517
+ def find_in_batches(batch_size: 1000)
518
+ @records.each_slice(batch_size) do |batch|
519
+ yield batch
520
+ end
521
+ end
522
+
523
+ def in_batches(of: 1000)
524
+ @records.each_slice(of) do |batch|
525
+ yield self.class.new(model_class, batch)
526
+ end
527
+ end
528
+
529
+ # =========================================================================
530
+ # Inspection
531
+ # =========================================================================
532
+
533
+ def inspect
534
+ "#<#{self.class.name} [#{@records.map(&:inspect).join(', ')}]>"
535
+ end
536
+
537
+ def to_s
538
+ inspect
539
+ end
540
+
541
+ # =========================================================================
542
+ # Scope support - AR calls _exec_scope for named scopes
543
+ # =========================================================================
544
+
545
+ # Called by AR when executing scopes
546
+ # Rails passes scope arguments and the scope body block
547
+ def _exec_scope(*args, &block)
548
+ # Execute the scope block in our context
549
+ # The block typically calls methods like `where`, `order`, etc.
550
+ result = instance_exec(*args, &block)
551
+
552
+ # Return the result if it's a CachedRelation, otherwise self
553
+ result.is_a?(CachedRelation) ? result : self
554
+ end
555
+
556
+ def scoping(skip_inherited_scope = false, full = nil, all_queries: nil, &block)
557
+ # Yield self for scoping blocks
558
+ yield self if block_given?
559
+ self
560
+ end
561
+
562
+ def _scoping(skip_inherited_scope = false, full = nil, all_queries: nil, &block)
563
+ scoping(skip_inherited_scope, full, all_queries: all_queries, &block)
564
+ end
565
+
566
+ # Allow method_missing for scope delegation
567
+ def respond_to_missing?(method_name, include_private = false)
568
+ model_class.respond_to?(method_name) || super
569
+ end
570
+
571
+ def method_missing(method_name, *args, **kwargs, &block)
572
+ # Try to delegate to model class scopes
573
+ if model_class.respond_to?(method_name)
574
+ # Execute the scope on a bare unscoped relation to get the where conditions
575
+ # This works because scopes add where clauses to the relation
576
+ begin
577
+ # Temporarily bypass Lyra's read overrides so scope calls go through AR
578
+ # Without this, scope lambdas that call `where(...)` would hit our override
579
+ # and return CachedRelation instead of building AR conditions
580
+ Thread.current[:lyra_bypass_read_override] = true
581
+ base_relation = model_class.unscoped
582
+ scope_result = base_relation.public_send(method_name, *args, **kwargs, &block)
583
+ rescue Lyra::StrictDataAccessViolation
584
+ # Don't swallow strict data access violations - these are intentional framework errors
585
+ raise
586
+ rescue => e
587
+ Rails.logger.debug("Lyra::CachedRelation: Could not execute scope #{method_name} - #{e.message}")
588
+ return self
589
+ ensure
590
+ Thread.current[:lyra_bypass_read_override] = nil
591
+ end
592
+
593
+ if scope_result.is_a?(ActiveRecord::Relation)
594
+ # Extract where conditions from the scope result
595
+ where_hash = extract_where_conditions(scope_result)
596
+ if where_hash.present?
597
+ return where(where_hash)
598
+ end
599
+ end
600
+
601
+ # Fallback: return self to allow chaining
602
+ self
603
+ else
604
+ super
605
+ end
606
+ end
607
+
608
+ def extract_where_conditions(relation)
609
+ # Try to extract hash conditions from the relation's where clause
610
+ return {} unless relation.respond_to?(:where_clause)
611
+
612
+ where_clause = relation.where_clause
613
+ return {} if where_clause.empty?
614
+
615
+ # Rails 7+ stores conditions in predicates
616
+ # Try to convert Arel predicates to a hash
617
+ conditions = {}
618
+ where_clause.send(:predicates).each do |predicate|
619
+ case predicate
620
+ when Arel::Nodes::Equality
621
+ # Simple equality: column = value
622
+ if predicate.left.respond_to?(:name)
623
+ column = predicate.left.name.to_sym
624
+ value = extract_predicate_value(predicate.right)
625
+ conditions[column] = value
626
+ end
627
+ when Arel::Nodes::In
628
+ # IN clause: column IN (values)
629
+ if predicate.left.respond_to?(:name)
630
+ column = predicate.left.name.to_sym
631
+ values = predicate.right.map { |v| extract_predicate_value(v) }
632
+ conditions[column] = values
633
+ end
634
+ end
635
+ end
636
+
637
+ conditions
638
+ rescue => e
639
+ Rails.logger.debug("Lyra::CachedRelation: Could not extract where conditions - #{e.message}")
640
+ {}
641
+ end
642
+
643
+ def extract_predicate_value(node)
644
+ case node
645
+ when Arel::Nodes::Casted
646
+ node.value
647
+ when Arel::Nodes::BindParam
648
+ # Rails 7+ bind params
649
+ node.value.value_before_type_cast
650
+ when NilClass
651
+ nil
652
+ else
653
+ node.respond_to?(:value) ? node.value : node
654
+ end
655
+ end
656
+
657
+ private
658
+
659
+ def matches_value?(record, key, value)
660
+ record_value = record.send(key)
661
+
662
+ case value
663
+ when Array
664
+ # Handle type coercion for arrays (e.g., array of string IDs vs integer column)
665
+ value.any? { |v| values_match?(record_value, v) }
666
+ when Range
667
+ value.cover?(record_value)
668
+ when nil
669
+ record_value.nil?
670
+ when Regexp
671
+ record_value.to_s.match?(value)
672
+ else
673
+ values_match?(record_value, value)
674
+ end
675
+ rescue NoMethodError
676
+ # Attribute doesn't exist on record
677
+ false
678
+ end
679
+
680
+ # Compare values with type coercion for common AR patterns
681
+ def values_match?(record_value, query_value)
682
+ return true if record_value == query_value
683
+
684
+ # Handle string/integer coercion (common with params)
685
+ if record_value.is_a?(Integer) && query_value.is_a?(String)
686
+ record_value == query_value.to_i
687
+ elsif record_value.is_a?(String) && query_value.is_a?(Integer)
688
+ record_value.to_i == query_value
689
+ # Handle boolean string coercion
690
+ elsif record_value.in?([true, false]) && query_value.is_a?(String)
691
+ record_value == ActiveModel::Type::Boolean.new.cast(query_value)
692
+ else
693
+ false
694
+ end
695
+ end
696
+
697
+ def compare_for_order(a, b, order_args)
698
+ order_args.each do |arg|
699
+ result = compare_single_order(a, b, arg)
700
+ return result unless result == 0
701
+ end
702
+ 0
703
+ end
704
+
705
+ def compare_single_order(a, b, arg)
706
+ case arg
707
+ when Symbol, String
708
+ compare_values(a.send(arg), b.send(arg))
709
+ when Hash
710
+ arg.each do |column, direction|
711
+ val_a = a.send(column)
712
+ val_b = b.send(column)
713
+ result = compare_values(val_a, val_b)
714
+ result = -result if direction.to_s.downcase == "desc"
715
+ return result unless result == 0
716
+ end
717
+ 0
718
+ else
719
+ 0
720
+ end
721
+ rescue NoMethodError
722
+ 0
723
+ end
724
+
725
+ def compare_values(a, b)
726
+ return 0 if a.nil? && b.nil?
727
+ return 1 if a.nil?
728
+ return -1 if b.nil?
729
+ a <=> b || 0
730
+ end
731
+
732
+ # =========================================================================
733
+ # WhereChain for where.not(...) support
734
+ # =========================================================================
735
+
736
+ class WhereChain
737
+ def initialize(relation)
738
+ @relation = relation
739
+ end
740
+
741
+ def not(conditions)
742
+ @relation.not(conditions)
743
+ end
744
+
745
+ def missing(*associations)
746
+ # Can't check missing associations in cached mode
747
+ @relation
748
+ end
749
+
750
+ def associated(*associations)
751
+ # Can't check associations in cached mode
752
+ @relation
753
+ end
754
+ end
755
+ end
756
+ end
757
+ end