activerecord-refined 0.8.1 → 0.10.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +111 -815
  4. data/activerecord-refined.gemspec +36 -17
  5. data/docs/conditions.md +206 -0
  6. data/docs/ctes.md +65 -0
  7. data/docs/expressions.md +125 -0
  8. data/docs/functions.md +219 -0
  9. data/docs/grouping.md +55 -0
  10. data/docs/joins.md +73 -0
  11. data/docs/json.md +230 -0
  12. data/docs/ordering.md +55 -0
  13. data/docs/time_zones.md +30 -0
  14. data/docs/windows.md +41 -0
  15. data/docs/writing.md +33 -0
  16. data/examples/aggregations.rb +31 -11
  17. data/examples/complex_joins.rb +12 -10
  18. data/examples/ctes.rb +22 -20
  19. data/examples/expressions.rb +110 -45
  20. data/examples/json.rb +77 -38
  21. data/examples/postgresql.rb +64 -53
  22. data/examples/predicates.rb +35 -33
  23. data/examples/subqueries.rb +20 -18
  24. data/examples/windows.rb +23 -21
  25. data/examples/writes.rb +26 -24
  26. data/lib/active_record/refined/ast.rb +1117 -373
  27. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  28. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  29. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  30. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  31. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  32. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  33. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  34. data/lib/active_record/refined/dialect.rb +340 -0
  35. data/lib/active_record/refined.rb +877 -307
  36. data/lib/activerecord-refined/version.rb +3 -1
  37. data/lib/activerecord-refined.rb +9 -5
  38. metadata +186 -15
  39. data/.github/workflows/push_gem.yml +0 -45
  40. data/.github/workflows/sandbox.yml +0 -295
  41. data/.github/workflows/test.yml +0 -90
  42. data/.gitignore +0 -19
  43. data/Gemfile +0 -12
  44. data/Rakefile +0 -25
  45. data/benchmark/query_building.rb +0 -129
  46. data/test/test_block_syntax.rb +0 -2493
  47. data/test/test_helper.rb +0 -221
data/.gitignore DELETED
@@ -1,19 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .ruby-version
6
- .yardoc
7
- Gemfile.lock
8
- InstalledFiles
9
- _yardoc
10
- coverage
11
- doc/
12
- lib/bundler/man
13
- pkg
14
- rdoc
15
- spec/reports
16
- t.rb
17
- test/tmp
18
- test/version_tmp
19
- tmp
data/Gemfile DELETED
@@ -1,12 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in activerecord-refined.gemspec
4
- gemspec
5
-
6
- # Only ADAPTER=postgresql and ADAPTER=mysql2 need these, and building them
7
- # needs the client libraries installed. `rake test` runs on SQLite, so a
8
- # checkout without them is still usable: bundle config set --local without db
9
- group :db do
10
- gem 'mysql2'
11
- gem 'pg'
12
- end
data/Rakefile DELETED
@@ -1,25 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- ADAPTERS = %w[sqlite3 postgresql mysql2].freeze
5
-
6
- Rake::TestTask.new do |t|
7
- t.test_files = FileList['test/test_*.rb']
8
- end
9
-
10
- namespace :test do
11
- ADAPTERS.each do |adapter|
12
- desc "Run the tests against #{adapter}"
13
- task adapter do
14
- puts "==== #{adapter} ===="
15
- ENV['ADAPTER'] = adapter
16
- Rake::Task[:test].reenable
17
- Rake::Task[:test].invoke
18
- end
19
- end
20
-
21
- desc "Run the tests against every adapter in turn"
22
- task all: ADAPTERS
23
- end
24
-
25
- task default: :test
@@ -1,129 +0,0 @@
1
- # Compares the cost of building the same queries through this gem's block
2
- # DSL and through Active Record's other argument styles: hash conditions,
3
- # string conditions, raw Arel, and relation and/or chains. Only query
4
- # construction (through to_sql) is measured; every style produces the same
5
- # SQL, which the script prints first as a sanity check.
6
- #
7
- # Run without bundler, so the profiling gems don't need to live in the
8
- # Gemfile:
9
- #
10
- # gem install benchmark-ips memory_profiler
11
- # ruby -Ilib benchmark/query_building.rb
12
-
13
- require "benchmark/ips"
14
- require "memory_profiler"
15
- require "active_record"
16
- require "activerecord-refined"
17
-
18
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
19
- ActiveRecord::Schema.verbose = false
20
- ActiveRecord::Schema.define do
21
- create_table(:users) {|t| t.string :name; t.integer :age }
22
- end
23
-
24
- class User < ActiveRecord::Base; end
25
-
26
- T = User.arel_table
27
-
28
- # Each variant must generate equivalent SQL; sanity-print once.
29
- VARIANTS = {
30
- "simple equality" => {
31
- "hash" => -> { User.where(name: "alice").to_sql },
32
- "string" => -> { User.where("name = ?", "alice").to_sql },
33
- "arel" => -> { User.where(T[:name].eq("alice")).to_sql },
34
- "block" => -> { User.where { :name == "alice" }.to_sql },
35
- },
36
- "range (BETWEEN)" => {
37
- "hash" => -> { User.where(age: 20..40).to_sql },
38
- "arel" => -> { User.where(T[:age].between(20..40)).to_sql },
39
- "block" => -> { User.where { :age.in?(20..40) }.to_sql },
40
- },
41
- "LIKE" => {
42
- "string" => -> { User.where("name LIKE ?", "ma%").to_sql },
43
- "arel" => -> { User.where(T[:name].matches("ma%", nil, true)).to_sql },
44
- "block" => -> { User.where { :name.like?("al%") }.to_sql },
45
- },
46
- "compound AND/OR" => {
47
- "string" => -> { User.where("age >= ? AND (name = ? OR name = ?)", 18, "alice", "bob").to_sql },
48
- "arel" => -> { User.where(T[:age].gteq(18).and(T[:name].eq("alice").or(T[:name].eq("bob")))).to_sql },
49
- "relation" => -> { User.where(age: 18..).and(User.where(name: "alice").or(User.where(name: "bob"))).to_sql },
50
- "block" => -> { User.where { (:age >= 18) & ((:name == "alice") | (:name == "bob")) }.to_sql },
51
- },
52
- }
53
-
54
- puts "=== generated SQL (sanity) ==="
55
- VARIANTS.each do |group, variants|
56
- puts "--- #{group} ---"
57
- variants.each {|name, thunk| puts " #{name.ljust(8)} #{thunk.call}" }
58
- end
59
-
60
- puts
61
- puts "=== speed (queries built per second) ==="
62
- VARIANTS.each do |group, variants|
63
- puts "--- #{group} ---"
64
- Benchmark.ips do |x|
65
- x.config(warmup: 0.5, time: 2)
66
- variants.each {|name, thunk| x.report(name, &thunk) }
67
- x.compare!
68
- end
69
- end
70
-
71
- puts
72
- puts "=== memory (per single call) ==="
73
- fmt = "%-18s %-9s %12s %12s"
74
- puts format(fmt, "group", "variant", "allocated B", "objects")
75
- VARIANTS.each do |group, variants|
76
- variants.each do |name, thunk|
77
- thunk.call # warm caches (schema, statement caches) outside the report
78
- report = MemoryProfiler.report { thunk.call }
79
- puts format(fmt, group, name, report.total_allocated_memsize, report.total_allocated)
80
- end
81
- end
82
-
83
- puts
84
- puts "=== Proc#refined ISeq copy (memory) ==="
85
- require "objspace"
86
-
87
- # Proc#refined runs the block under the refinements by deep-copying its
88
- # instruction sequence, nested blocks included. The copy is made lazily on
89
- # the refined proc's first call and memoized per source iseq and refinement
90
- # list for the life of the VM, so it is paid once per block call site, not
91
- # per query.
92
- iseq_count = -> {
93
- counts = ObjectSpace.count_imemo_objects
94
- counts[:imemo_iseq] || counts[:iseq]
95
- }
96
- context = ActiveRecord::Refined::BlockContext.new
97
- syntax = ActiveRecord::Refined::BlockSyntax
98
-
99
- {
100
- "simple equality" => proc { :name == "alice" },
101
- "compound AND/OR" => proc { (:age >= 18) & ((:name == "alice") | (:name == "bob")) },
102
- }.each do |label, blk|
103
- refined = blk.refined(syntax)
104
- context.instance_exec(&refined) # the copy is made here, on the first call
105
- original = ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(blk))
106
- copy = ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(refined))
107
- puts "#{label}: original iseq #{original} B, refined copy #{copy} B"
108
- end
109
-
110
- make_proc = -> { proc { :age > 20 } }
111
- context.instance_exec(&make_proc.call.refined(syntax))
112
- before = iseq_count.call
113
- 1000.times { context.instance_exec(&make_proc.call.refined(syntax)) }
114
- puts "1000 more calls from the same call site copied #{iseq_count.call - before} iseqs"
115
-
116
- puts
117
- puts "=== where the block path spends its time ==="
118
- block = proc { :name == "alice" }
119
- refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
120
- context = ActiveRecord::Refined::BlockContext.new
121
- Benchmark.ips do |x|
122
- x.config(warmup: 0.5, time: 2)
123
- x.report("Proc#refined alone") { block.refined(ActiveRecord::Refined::BlockSyntax) }
124
- x.report("instance_exec of pre-refined proc") { context.instance_exec(&refined_block) }
125
- x.report("refined + instance_exec") {
126
- ActiveRecord::Refined::BlockContext.new.instance_exec(&block.refined(ActiveRecord::Refined::BlockSyntax))
127
- }
128
- x.compare!
129
- end