arlj 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 674e9b2df96b4850dc128bc5de58252833a7ba95
4
+ data.tar.gz: aa92466cd390f09ad567acbad0a05a13cdfa995f
5
+ SHA512:
6
+ metadata.gz: f6498872bd416cb091ab5a69ded518af390097483583a72bb00217598373da17f608f2234ce49376978673acbd7b5e479120f1567f261ba0270ad43e449b1253
7
+ data.tar.gz: 6ed666eb60114b8225dd5ad04e51cc07094cbd81e3be9f8170af3c38e30606c2c7424f2c58aa651dcb17ac6bc4488b52547a9dbbd3f6737260c72c7ffd1ff368
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in arlj.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 fengb
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Arlj - ActiveRecord Left Join
2
+
3
+ Make left joins feel like first-class citizens in ActiveRecord.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'arlj'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install arlj
20
+
21
+ ## Usage
22
+
23
+ Load Arlj into your class:
24
+
25
+ ```ruby
26
+ class Parent < ActiveRecord::Base
27
+ extend Arlj
28
+ end
29
+ ```
30
+
31
+ Or extend all of ActiveRecord models:
32
+
33
+ ```ruby
34
+ ActiveRecord::Base.extend Arlj
35
+ ```
36
+
37
+ Then begin to left join!
38
+
39
+ ```ruby
40
+ puts Parent.arlj(:children).group('records.id').select('COUNT(children.id)').to_sql
41
+ => SELECT COUNT(children.id)
42
+ FROM "parents"
43
+ LEFT OUTER JOIN "children"
44
+ ON "children"."parent_id" = "parents"."id"
45
+ GROUP BY records.id
46
+ ```
47
+
48
+ `arlj` is purposely low level to be extra chainable.
49
+
50
+ Arlj also adds an aggregation syntax:
51
+
52
+ ```ruby
53
+ Parent.arlj_aggregate(:children, 'COUNT(*)', 'SUM(col)' => 'total').select('children_count', 'total').to_sql
54
+ => SELECT children_count
55
+ , total
56
+ FROM "parents"
57
+ LEFT OUTER JOIN (SELECT "children"."parent_id"
58
+ , COUNT("children"."id") AS children_count
59
+ , SUM("children"."col") AS total
60
+ FROM "children"
61
+ GROUP BY "children"."parent_id") arlj_aggregate_children
62
+ ON arlj_aggregate_children."parent_id" = "parents"."id"
63
+ ```
64
+
65
+ `arlj_aggregate` currently uses a subquery to hide its aggregation. It's not the
66
+ most efficient implementation but it does offer a much better chaining
67
+ experience than using `group` at the top level.
68
+
69
+ Arlj can also alias `arlj` and `arlj_aggregate` to `left_joins` and
70
+ `left_joins_aggregate` respectively:
71
+
72
+ ```ruby
73
+ class Parent < ActiveRecord::Base
74
+ extend Arlj::LeftJoins
75
+ end
76
+
77
+ Parent.left_joins(:children).group('records.id').select('COUNT(children.id)')
78
+ Parent.left_joins_aggregate(:children, 'COUNT(*)', 'SUM(col)' => 'total')
79
+ ```
80
+
81
+ ## TODO
82
+
83
+ * Relations with conditions
84
+ * `LEFT JOIN [...] ON`
85
+ * `has_and_belongs_to_many`
86
+ * `has_many :through =>`
87
+
88
+ ## Contributing
89
+
90
+ 1. Fork it ( https://github.com/fengb/arlj/fork )
91
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
92
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
93
+ 4. Push to the branch (`git push origin my-new-feature`)
94
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/arlj.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'arlj/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'arlj'
8
+ spec.version = Arlj::VERSION
9
+ spec.authors = ['Benjamin Feng']
10
+ spec.email = ['contact@fengb.info']
11
+ spec.summary = %q{ActiveRecord Left Join}
12
+ spec.description = %q{Make left joins feel like first-class citizens in ActiveRecord.}
13
+ spec.homepage = 'https://github.com/fengb/arlj'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'activerecord', '>= 3.1'
22
+ spec.add_runtime_dependency 'memoist', '~> 0.11.0'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.7'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'temping', '~> 3.2'
28
+ end
@@ -0,0 +1,10 @@
1
+ require 'arlj'
2
+
3
+ module Arlj
4
+ module LeftJoins
5
+ include Arlj
6
+
7
+ alias_method :left_joins, :arlj
8
+ alias_method :left_joins_aggregate, :arlj_aggregate
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Arlj
2
+ VERSION = "0.0.1"
3
+ end
data/lib/arlj.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'memoist'
2
+
3
+ module Arlj
4
+ autoload :LeftJoins, 'arlj/left_joins'
5
+ autoload :VERSION, 'arlj/version'
6
+
7
+ extend Memoist
8
+
9
+ def arlj(assoc)
10
+ # Example snippet:
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
97
+ end
98
+ end
data/spec/arlj_spec.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ require 'arlj'
4
+ require 'temping'
5
+
6
+ RSpec.describe Arlj do
7
+ Temping.create :parent do
8
+ with_columns do |t|
9
+ t.string :name
10
+ end
11
+
12
+ extend Arlj
13
+
14
+ has_many :children
15
+ end
16
+
17
+ Temping.create :child do
18
+ with_columns do |t|
19
+ t.integer :parent_id
20
+ t.integer :col
21
+ end
22
+ end
23
+
24
+ before(:all) do
25
+ @parent = Parent.create(name: 'John')
26
+ (1..10).each do |n|
27
+ @parent.children.create(col: n)
28
+ end
29
+
30
+ @parent_no_child = Parent.create(name: 'Jane')
31
+ end
32
+
33
+ describe '#arlj' do
34
+ it 'joins the other table' do
35
+ counts = Parent.arlj(:children).
36
+ group('parents.id').
37
+ pluck('COUNT(children.id)')
38
+ assert{ counts.sort == [0, 10] }
39
+ end
40
+ end
41
+
42
+ describe '#arlj_aggregate' do
43
+ specify 'COUNT(*)' do
44
+ children_count = Parent.arlj_aggregate(:children, 'count(*)').pluck('children_count').first
45
+ assert{ children_count == @parent.children.size }
46
+ end
47
+
48
+ specify 'sum(col)' do
49
+ children_sum_col = Parent.arlj_aggregate(:children, 'SUM(col)').pluck('children_sum_col').first
50
+ assert{ children_sum_col == @parent.children.sum(:col) }
51
+ end
52
+
53
+ specify 'SUM(col) => name' do
54
+ sum = Parent.arlj_aggregate(:children, 'sum(col)' => 'sum').pluck('sum').first
55
+ assert{ sum == @parent.children.sum(:col) }
56
+ end
57
+
58
+ specify 'FAKE(col) raises error' do
59
+ error = rescuing{ Parent.arlj_aggregate(:children, 'FAKE(col)') }
60
+ assert{ error }
61
+ end
62
+
63
+ specify 'COUNT(*) => count, SUM(col) => sum' do
64
+ array = Parent.arlj_aggregate(:children, 'count(*)' => 'count', 'sum(col)' => 'sum').pluck('count', 'sum').first
65
+ assert{ array[0] == @parent.children.size }
66
+ assert{ array[1] == @parent.children.sum(:col) }
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ require 'arlj/left_joins'
4
+
5
+ RSpec.describe Arlj::LeftJoins do
6
+ class LjParent < Parent
7
+ extend Arlj::LeftJoins
8
+ end
9
+
10
+ specify '#left_joins does arlj stuff' do
11
+ counts = LjParent.left_joins(:children).
12
+ group('parents.id').
13
+ pluck('COUNT(children.id)')
14
+ assert{ counts.sort == [0, 10] }
15
+ end
16
+
17
+ specify '#left_joins_aggregate does arlj_aggregate stuff' do
18
+ children_count = LjParent.left_joins_aggregate(:children, 'count(*)').
19
+ pluck('children_count').
20
+ first
21
+ assert{ children_count == 10 }
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_record'
2
+ require 'wrong/adapters/rspec'
3
+
4
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
5
+
6
+ RSpec.configure do |c|
7
+ c.include Wrong
8
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arlj
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Feng
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: temping
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.2'
97
+ description: Make left joins feel like first-class citizens in ActiveRecord.
98
+ email:
99
+ - contact@fengb.info
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - arlj.gemspec
110
+ - lib/arlj.rb
111
+ - lib/arlj/left_joins.rb
112
+ - lib/arlj/version.rb
113
+ - spec/arlj_spec.rb
114
+ - spec/left_joins_spec.rb
115
+ - spec/spec_helper.rb
116
+ homepage: https://github.com/fengb/arlj
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.2.2
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: ActiveRecord Left Join
140
+ test_files:
141
+ - spec/arlj_spec.rb
142
+ - spec/left_joins_spec.rb
143
+ - spec/spec_helper.rb