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.
- checksums.yaml +4 -4
- data/.yardopts +17 -0
- data/README.md +111 -815
- data/activerecord-refined.gemspec +36 -17
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +31 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +110 -45
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +1117 -373
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +877 -307
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +9 -5
- metadata +186 -15
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -90
- data/.gitignore +0 -19
- data/Gemfile +0 -12
- data/Rakefile +0 -25
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2493
- data/test/test_helper.rb +0 -221
data/.gitignore
DELETED
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
|
data/benchmark/query_building.rb
DELETED
|
@@ -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
|