arlj 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -0
- data/LICENSE.txt +1 -1
- data/README.md +19 -12
- data/Rakefile +5 -1
- data/arlj.gemspec +1 -1
- data/lib/arlj/base.rb +106 -0
- data/lib/arlj/version.rb +1 -1
- data/lib/arlj.rb +9 -92
- data/spec/{arlj_spec.rb → base_spec.rb} +2 -2
- data/spec/left_joins_spec.rb +3 -3
- metadata +20 -19
- data/lib/arlj/left_joins.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a58db94fac67a5fa42539b8b1cc30beaf1134207
|
4
|
+
data.tar.gz: 3ffd40c1670e27ed4e6eefe82b20df628dd7b361
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: caf5c2c6a30881344c5f75074a5a1131263f68614574b28f3790a475008d638cd8745dbdbc3c0f0d7085d4bc863d112b310020b14ba06d3fa3613d133ef803d8
|
7
|
+
data.tar.gz: 5276383d71fdf062b199cd3370dfac5258fccf7856f5a753e8444dd5a741ab8d64ec19ba2c34b79d005e14d0286502ee4f89108bb5e5d49a159267ab7e2237ff
|
data/.travis.yml
ADDED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Arlj - ActiveRecord Left Join
|
1
|
+
# Arlj - ActiveRecord Left Join [![Travis CI](https://travis-ci.org/fengb/arlj.svg?branch=master)](https://travis-ci.org/fengb/arlj)
|
2
2
|
|
3
3
|
Make left joins feel like first-class citizens in ActiveRecord.
|
4
4
|
|
@@ -37,7 +37,7 @@ ActiveRecord::Base.extend Arlj
|
|
37
37
|
Then begin to left join!
|
38
38
|
|
39
39
|
```ruby
|
40
|
-
puts Parent.
|
40
|
+
puts Parent.left_joins(:children).group('records.id').select('COUNT(children.id)').to_sql
|
41
41
|
=> SELECT COUNT(children.id)
|
42
42
|
FROM "parents"
|
43
43
|
LEFT OUTER JOIN "children"
|
@@ -45,12 +45,12 @@ puts Parent.arlj(:children).group('records.id').select('COUNT(children.id)').to_
|
|
45
45
|
GROUP BY records.id
|
46
46
|
```
|
47
47
|
|
48
|
-
`
|
48
|
+
`left_joins` is purposely low level to be extra chainable.
|
49
49
|
|
50
|
-
Arlj also adds an aggregation
|
50
|
+
Arlj also adds an aggregation method:
|
51
51
|
|
52
52
|
```ruby
|
53
|
-
Parent.
|
53
|
+
Parent.left_joins_aggregate(:children, 'COUNT(*)', 'SUM(col)' => 'total').select('children_count', 'total').to_sql
|
54
54
|
=> SELECT children_count
|
55
55
|
, total
|
56
56
|
FROM "parents"
|
@@ -62,22 +62,29 @@ Parent.arlj_aggregate(:children, 'COUNT(*)', 'SUM(col)' => 'total').select('chil
|
|
62
62
|
ON arlj_aggregate_children."parent_id" = "parents"."id"
|
63
63
|
```
|
64
64
|
|
65
|
-
`
|
66
|
-
most efficient implementation but it does offer a much better chaining
|
65
|
+
`left_joins_aggregate` currently uses a subquery to hide its aggregation. It is
|
66
|
+
not the most efficient implementation but it does offer a much better chaining
|
67
67
|
experience than using `group` at the top level.
|
68
68
|
|
69
|
-
|
70
|
-
`left_joins_aggregate` respectively
|
69
|
+
If you prefer, you may also use `arlj` and `arlj_aggregate` instead of
|
70
|
+
`left_joins` and `left_joins_aggregate` respectively. To prevent potential
|
71
|
+
naming conflicts, please use `Arlj::Base`:
|
71
72
|
|
72
73
|
```ruby
|
73
74
|
class Parent < ActiveRecord::Base
|
74
|
-
extend Arlj::
|
75
|
+
extend Arlj::Base
|
75
76
|
end
|
77
|
+
```
|
78
|
+
|
79
|
+
**Arlj** has an experimental flag that uses the **memoist** gem to memoize the
|
80
|
+
generated join SQL:
|
76
81
|
|
77
|
-
|
78
|
-
|
82
|
+
```ruby
|
83
|
+
Arlj.memoize!
|
79
84
|
```
|
80
85
|
|
86
|
+
This has not been proven to be faster.
|
87
|
+
|
81
88
|
## TODO
|
82
89
|
|
83
90
|
* Relations with conditions
|
data/Rakefile
CHANGED
data/arlj.gemspec
CHANGED
@@ -19,10 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
21
|
spec.add_runtime_dependency 'activerecord', '>= 3.1'
|
22
|
-
spec.add_runtime_dependency 'memoist', '~> 0.11.0'
|
23
22
|
|
24
23
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
24
|
spec.add_development_dependency 'rake', '~> 10.0'
|
26
25
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
27
26
|
spec.add_development_dependency 'temping', '~> 3.2'
|
27
|
+
spec.add_development_dependency 'wrong', '~> 0.7'
|
28
28
|
end
|
data/lib/arlj/base.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Arlj
|
2
|
+
module Base
|
3
|
+
def self.memoize!
|
4
|
+
require 'memoist'
|
5
|
+
self.extend Memoist
|
6
|
+
self.memoize :arlj_sql, :arlj_aggregate_sql
|
7
|
+
end
|
8
|
+
|
9
|
+
def arlj(assoc)
|
10
|
+
# Example snippet:
|
11
|
+
# LEFT JOIN [assoc]
|
12
|
+
# ON [assoc].source_id = source.id
|
13
|
+
|
14
|
+
joins(arlj_sql(assoc))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Example usage:
|
18
|
+
# arlj_aggregate(other, 'count(*)', 'sum(col)' => target_name)
|
19
|
+
def arlj_aggregate(assoc, *args)
|
20
|
+
# Example snippet:
|
21
|
+
# LEFT JOIN(SELECT source_id, [func]([column]) AS [target_name]
|
22
|
+
# FROM [assoc]
|
23
|
+
# GROUP BY [assoc].source_id) arlj_aggregate_[assoc]
|
24
|
+
# ON [assoc].source_id = source.id
|
25
|
+
|
26
|
+
joins(arlj_aggregate_sql(assoc, *args))
|
27
|
+
end
|
28
|
+
|
29
|
+
def arlj_arel(assoc)
|
30
|
+
refl = reflect_on_association(assoc)
|
31
|
+
arlj_left_join_arel(refl.klass.arel_table, refl.foreign_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def arlj_sql(assoc)
|
35
|
+
arlj_arel(assoc).join_sources
|
36
|
+
end
|
37
|
+
|
38
|
+
def arlj_aggregate_arel(assoc, *args)
|
39
|
+
options = args.extract_options!
|
40
|
+
|
41
|
+
refl = reflect_on_association(assoc)
|
42
|
+
refl_arel = refl.klass.arel_table
|
43
|
+
|
44
|
+
join_name = "arlj_aggregate_#{refl.table_name}"
|
45
|
+
|
46
|
+
columns = [refl_arel[refl.foreign_key]]
|
47
|
+
args.each do |thunk|
|
48
|
+
columns << parse_thunk(refl, assoc, refl_arel, thunk)
|
49
|
+
end
|
50
|
+
options.each do |thunk, name|
|
51
|
+
columns << parse_thunk(refl, assoc, refl_arel, thunk, name)
|
52
|
+
end
|
53
|
+
|
54
|
+
subq_arel =
|
55
|
+
refl_arel.project(columns).
|
56
|
+
from(refl_arel).
|
57
|
+
group(refl_arel[refl.foreign_key]).
|
58
|
+
as(join_name)
|
59
|
+
|
60
|
+
arlj_left_join_arel(subq_arel, refl.foreign_key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def arlj_aggregate_sql(assoc, *args)
|
64
|
+
arlj_aggregate_arel(assoc, *args).join_sources
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
THUNK_PATTERN = /^([a-zA-Z]*)\((.*)\)$/
|
70
|
+
AGGREGATE_FUNCTIONS = {
|
71
|
+
'sum' => 'sum',
|
72
|
+
'average' => 'average',
|
73
|
+
'avg' => 'average',
|
74
|
+
'maximum' => 'maximum',
|
75
|
+
'max' => 'maximum',
|
76
|
+
'minimum' => 'minimum',
|
77
|
+
'min' => 'minimum',
|
78
|
+
'count' => 'count',
|
79
|
+
}.freeze
|
80
|
+
def parse_thunk(refl, assoc, arel, thunk, name=nil)
|
81
|
+
matchdata = THUNK_PATTERN.match(thunk)
|
82
|
+
if matchdata.nil?
|
83
|
+
raise "'#{thunk}' not parsable - must be of format 'func(column)'"
|
84
|
+
end
|
85
|
+
|
86
|
+
func = AGGREGATE_FUNCTIONS[matchdata[1].downcase]
|
87
|
+
if func.nil?
|
88
|
+
raise "'#{matchdata[1]}' not recognized - must be one of #{AGGREGATE_FUNCTIONS.keys}"
|
89
|
+
end
|
90
|
+
|
91
|
+
if matchdata[2] == '*'
|
92
|
+
column = refl.active_record_primary_key
|
93
|
+
name ||= "#{assoc}_#{func}"
|
94
|
+
else
|
95
|
+
column = matchdata[2]
|
96
|
+
name ||= "#{assoc}_#{func}_#{column}"
|
97
|
+
end
|
98
|
+
arel[column].send(func).as(name)
|
99
|
+
end
|
100
|
+
|
101
|
+
def arlj_left_join_arel(arel, foreign_key)
|
102
|
+
arel_table.join(arel, Arel::Nodes::OuterJoin).
|
103
|
+
on(arel[foreign_key].eq(arel_table[self.primary_key]))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/arlj/version.rb
CHANGED
data/lib/arlj.rb
CHANGED
@@ -1,98 +1,15 @@
|
|
1
|
-
require '
|
1
|
+
require 'arlj/base'
|
2
2
|
|
3
3
|
module Arlj
|
4
|
-
autoload :
|
5
|
-
|
4
|
+
autoload :Version, 'arlj/version'
|
5
|
+
include Arlj::Base
|
6
6
|
|
7
|
-
|
7
|
+
alias_method :left_joins, :arlj
|
8
|
+
alias_method :left_joins_aggregate, :arlj_aggregate
|
9
|
+
alias_method :left_joins_arel, :arlj_arel
|
10
|
+
alias_method :left_joins_aggregate_arel, :arlj_aggregate_arel
|
8
11
|
|
9
|
-
def
|
10
|
-
|
11
|
-
# LEFT JOIN [assoc]
|
12
|
-
# ON [assoc].source_id = source.id
|
13
|
-
|
14
|
-
refl = reflect_on_association(assoc)
|
15
|
-
sources = arlj_arel_sources(refl.klass.arel_table, refl.foreign_key)
|
16
|
-
joins(sources)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Example usage:
|
20
|
-
# arlj_aggregate(other, 'count(*)', 'sum(col)' => target_name)
|
21
|
-
def arlj_aggregate(assoc, *args)
|
22
|
-
# Example snippet:
|
23
|
-
# LEFT JOIN(SELECT source_id, [func]([column]) AS [target_name]
|
24
|
-
# FROM [assoc]
|
25
|
-
# GROUP BY [assoc].source_id) arlj_aggregate_[assoc]
|
26
|
-
# ON [assoc].source_id = source.id
|
27
|
-
|
28
|
-
sources = arlj_aggregate_sources(assoc, *args)
|
29
|
-
joins(sources)
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
THUNK_PATTERN = /^([a-zA-Z]*)\((.*)\)$/
|
35
|
-
AGGREGATE_FUNCTIONS = {
|
36
|
-
'sum' => 'sum',
|
37
|
-
'average' => 'average',
|
38
|
-
'avg' => 'average',
|
39
|
-
'maximum' => 'maximum',
|
40
|
-
'max' => 'maximum',
|
41
|
-
'minimum' => 'minimum',
|
42
|
-
'min' => 'minimum',
|
43
|
-
'count' => 'count',
|
44
|
-
}.freeze
|
45
|
-
def parse_thunk(refl, assoc, arel, thunk, name=nil)
|
46
|
-
matchdata = THUNK_PATTERN.match(thunk)
|
47
|
-
if matchdata.nil?
|
48
|
-
raise "'#{thunk}' not parsable - must be of format 'func(column)'"
|
49
|
-
end
|
50
|
-
|
51
|
-
func = AGGREGATE_FUNCTIONS[matchdata[1].downcase]
|
52
|
-
if func.nil?
|
53
|
-
raise "'#{matchdata[1]}' not recognized - must be one of #{AGGREGATE_FUNCTIONS.keys}"
|
54
|
-
end
|
55
|
-
|
56
|
-
if matchdata[2] == '*'
|
57
|
-
column = refl.active_record_primary_key
|
58
|
-
name ||= "#{assoc}_#{func}"
|
59
|
-
else
|
60
|
-
column = matchdata[2]
|
61
|
-
name ||= "#{assoc}_#{func}_#{column}"
|
62
|
-
end
|
63
|
-
arel[column].send(func).as(name)
|
64
|
-
end
|
65
|
-
|
66
|
-
def arlj_aggregate_sources(assoc, *args)
|
67
|
-
options = args.extract_options!
|
68
|
-
|
69
|
-
refl = reflect_on_association(assoc)
|
70
|
-
refl_arel = refl.klass.arel_table
|
71
|
-
|
72
|
-
join_name = "arlj_aggregate_#{refl.table_name}"
|
73
|
-
|
74
|
-
columns = [refl_arel[refl.foreign_key]]
|
75
|
-
args.each do |thunk|
|
76
|
-
columns << parse_thunk(refl, assoc, refl_arel, thunk)
|
77
|
-
end
|
78
|
-
options.each do |thunk, name|
|
79
|
-
columns << parse_thunk(refl, assoc, refl_arel, thunk, name)
|
80
|
-
end
|
81
|
-
|
82
|
-
subq_arel =
|
83
|
-
refl_arel.project(columns).
|
84
|
-
from(refl_arel).
|
85
|
-
group(refl_arel[refl.foreign_key]).
|
86
|
-
as(join_name)
|
87
|
-
|
88
|
-
arlj_arel_sources(subq_arel, refl.foreign_key)
|
89
|
-
end
|
90
|
-
memoize :arlj_aggregate_sources
|
91
|
-
|
92
|
-
def arlj_arel_sources(arel, foreign_key)
|
93
|
-
arel_join =
|
94
|
-
arel_table.join(arel, Arel::Nodes::OuterJoin).
|
95
|
-
on(arel[foreign_key].eq(arel_table[self.primary_key]))
|
96
|
-
arel_join.join_sources
|
12
|
+
def self.memoize!
|
13
|
+
Arlj::Base.memoize!
|
97
14
|
end
|
98
15
|
end
|
@@ -3,13 +3,13 @@ require 'spec_helper'
|
|
3
3
|
require 'arlj'
|
4
4
|
require 'temping'
|
5
5
|
|
6
|
-
RSpec.describe Arlj do
|
6
|
+
RSpec.describe Arlj::Base do
|
7
7
|
Temping.create :parent do
|
8
8
|
with_columns do |t|
|
9
9
|
t.string :name
|
10
10
|
end
|
11
11
|
|
12
|
-
extend Arlj
|
12
|
+
extend Arlj::Base
|
13
13
|
|
14
14
|
has_many :children
|
15
15
|
end
|
data/spec/left_joins_spec.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
require 'arlj
|
3
|
+
require 'arlj'
|
4
4
|
|
5
|
-
RSpec.describe Arlj
|
5
|
+
RSpec.describe Arlj, 'left_joins' do
|
6
6
|
class LjParent < Parent
|
7
|
-
extend Arlj
|
7
|
+
extend Arlj
|
8
8
|
end
|
9
9
|
|
10
10
|
specify '#left_joins does arlj stuff' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arlj
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Feng
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-12-
|
11
|
+
date: 2014-12-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.1'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: memoist
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: 0.11.0
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: 0.11.0
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: bundler
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +80,20 @@ dependencies:
|
|
94
80
|
- - "~>"
|
95
81
|
- !ruby/object:Gem::Version
|
96
82
|
version: '3.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: wrong
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.7'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.7'
|
97
97
|
description: Make left joins feel like first-class citizens in ActiveRecord.
|
98
98
|
email:
|
99
99
|
- contact@fengb.info
|
@@ -102,15 +102,16 @@ extensions: []
|
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
104
|
- ".gitignore"
|
105
|
+
- ".travis.yml"
|
105
106
|
- Gemfile
|
106
107
|
- LICENSE.txt
|
107
108
|
- README.md
|
108
109
|
- Rakefile
|
109
110
|
- arlj.gemspec
|
110
111
|
- lib/arlj.rb
|
111
|
-
- lib/arlj/
|
112
|
+
- lib/arlj/base.rb
|
112
113
|
- lib/arlj/version.rb
|
113
|
-
- spec/
|
114
|
+
- spec/base_spec.rb
|
114
115
|
- spec/left_joins_spec.rb
|
115
116
|
- spec/spec_helper.rb
|
116
117
|
homepage: https://github.com/fengb/arlj
|
@@ -138,6 +139,6 @@ signing_key:
|
|
138
139
|
specification_version: 4
|
139
140
|
summary: ActiveRecord Left Join
|
140
141
|
test_files:
|
141
|
-
- spec/
|
142
|
+
- spec/base_spec.rb
|
142
143
|
- spec/left_joins_spec.rb
|
143
144
|
- spec/spec_helper.rb
|