joiner 0.3.4 → 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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/.travis.yml +9 -0
- data/README.md +6 -4
- data/Rakefile +5 -0
- data/joiner.gemspec +6 -6
- data/lib/joiner.rb +4 -0
- data/lib/joiner/alias_tracker.rb +84 -0
- data/lib/joiner/join_aliaser.rb +41 -0
- data/lib/joiner/join_dependency.rb +11 -0
- data/lib/joiner/joins.rb +15 -20
- data/spec/spec_helper.rb +1 -1
- metadata +21 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 50d83b6e1af0b5ef8c29fbbaabbb56a8c33ed99acbf57f507d13bcff673d92b2
|
4
|
+
data.tar.gz: 62adfb53520243ff108e85f68c61ca121cf28f1e6344fac539aa36c725efe0c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f96a6e7b1165be044ae4fba8dd71f9950f6635cbac352d7aa9d0aea9f7ead03d8bc0928f4fa69422539e4945976207394efc86704556c5fe44b8bcbda9ac2106
|
7
|
+
data.tar.gz: fa5f47d42b4d823581706dc5644b1b7b0ff4b47232f4216667c9526065e01fa13d1d1f46424e815f6df4bfc42b5659a56dc4831e5af1928ca3151d63bd0a7b5f
|
@@ -0,0 +1,23 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
|
9
|
+
strategy:
|
10
|
+
fail-fast: false
|
11
|
+
matrix:
|
12
|
+
ruby: [ '2.5', '2.6', '2.7' ]
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- name: Check out code
|
16
|
+
uses: actions/checkout@v2
|
17
|
+
- name: Set up ruby
|
18
|
+
uses: ruby/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
21
|
+
bundler-cache: true
|
22
|
+
- name: Test
|
23
|
+
run: "bundle exec rspec"
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
This gem, abstracted out from [Thinking Sphinx](http://pat.github.io/thinking-sphinx), turns a bunch of association trees from the perspective of a single model and builds a bunch of OUTER JOINs that can be passed into ActiveRecord::Relation's `join` method. You can also find out the generated table aliases for each join, in case you're referring to columns from those joins at some other point.
|
4
4
|
|
5
|
-
If this gem is used by anyone other than myself/Thinking Sphinx, I'll be surprised. My reason for pulling it out is so I can more cleanly support Rails' changing approaches to join generation (see v3.1-
|
5
|
+
If this gem is used by anyone other than myself/Thinking Sphinx, I'll be surprised. My reason for pulling it out is so I can more cleanly support Rails' changing approaches to join generation (see v3.1-v4.0 compared to v4.1-v5.1 compared to v5.2).
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
9
|
It's a gem - so you can either install it yourself, or add it to the appropriate Gemfile or gemspec.
|
10
10
|
|
11
11
|
```term
|
12
|
-
gem install joiner --version 0.
|
12
|
+
gem install joiner --version 0.6.0
|
13
13
|
```
|
14
14
|
|
15
15
|
## Usage
|
@@ -17,7 +17,7 @@ gem install joiner --version 0.3.4
|
|
17
17
|
First, create a join collection, based on an ActiveRecord model:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
joiner = Joiner::Joins.new
|
20
|
+
joiner = Joiner::Joins.new User
|
21
21
|
```
|
22
22
|
|
23
23
|
Then you can add joins for a given association path. For example, if User has many articles, and articles have many comments:
|
@@ -49,6 +49,8 @@ path.model #=> Comment
|
|
49
49
|
|
50
50
|
## Contributing
|
51
51
|
|
52
|
+
Please note that this project now has a [Contributor Code of Conduct](http://contributor-covenant.org/version/1/0/0/). By participating in this project you agree to abide by its terms.
|
53
|
+
|
52
54
|
1. Fork it
|
53
55
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
54
56
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
@@ -57,4 +59,4 @@ path.model #=> Comment
|
|
57
59
|
|
58
60
|
## Licence
|
59
61
|
|
60
|
-
Copyright (c) 2013, Joiner is developed and maintained by [Pat Allan](http://freelancing-gods.com), and is released under the open MIT Licence.
|
62
|
+
Copyright (c) 2013-2020, Joiner is developed and maintained by [Pat Allan](http://freelancing-gods.com), and is released under the open MIT Licence.
|
data/Rakefile
CHANGED
data/joiner.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
Gem::Specification.new do |spec|
|
3
3
|
spec.name = 'joiner'
|
4
|
-
spec.version = '0.
|
4
|
+
spec.version = '0.6.0'
|
5
5
|
spec.authors = ['Pat Allan']
|
6
6
|
spec.email = ['pat@freelancing-gods.com']
|
7
7
|
spec.summary = %q{Builds ActiveRecord joins from association paths}
|
@@ -13,10 +13,10 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.test_files = spec.files.grep(%r{^(spec)/})
|
14
14
|
spec.require_paths = ['lib']
|
15
15
|
|
16
|
-
spec.add_runtime_dependency 'activerecord', '>=
|
16
|
+
spec.add_runtime_dependency 'activerecord', '>= 6.1.0'
|
17
17
|
|
18
|
-
spec.add_development_dependency 'combustion', '~>
|
19
|
-
spec.add_development_dependency 'rails', '>=
|
20
|
-
spec.add_development_dependency 'rspec-rails', '~>
|
21
|
-
spec.add_development_dependency 'sqlite3', '~> 1.
|
18
|
+
spec.add_development_dependency 'combustion', '~> 1.1'
|
19
|
+
spec.add_development_dependency 'rails', '>= 6.1.0'
|
20
|
+
spec.add_development_dependency 'rspec-rails', '~> 4'
|
21
|
+
spec.add_development_dependency 'sqlite3', '~> 1.4'
|
22
22
|
end
|
data/lib/joiner.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require 'set'
|
2
|
+
require 'active_record'
|
2
3
|
|
3
4
|
module Joiner
|
4
5
|
class AssociationNotFound < StandardError
|
5
6
|
end
|
6
7
|
end
|
7
8
|
|
9
|
+
require 'joiner/alias_tracker'
|
10
|
+
require 'joiner/join_aliaser'
|
11
|
+
require 'joiner/join_dependency'
|
8
12
|
require 'joiner/joins'
|
9
13
|
require 'joiner/path'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/conversions"
|
4
|
+
|
5
|
+
# This code is taken straight from Rails, prior to v6.1.0.
|
6
|
+
# I'm maintaining a copy here to save myself having to work through aliasing
|
7
|
+
# logic myself - there's a good chance I don't need all of thiis, but it'll do
|
8
|
+
# to get this gem working with Rails 6.1.
|
9
|
+
|
10
|
+
class Joiner::AliasTracker # :nodoc:
|
11
|
+
def self.create(connection, initial_table, joins, aliases = nil)
|
12
|
+
if joins.empty?
|
13
|
+
aliases ||= Hash.new(0)
|
14
|
+
elsif aliases
|
15
|
+
default_proc = aliases.default_proc || proc { 0 }
|
16
|
+
aliases.default_proc = proc { |h, k|
|
17
|
+
h[k] = initial_count_for(connection, k, joins) + default_proc.call(h, k)
|
18
|
+
}
|
19
|
+
else
|
20
|
+
aliases = Hash.new { |h, k|
|
21
|
+
h[k] = initial_count_for(connection, k, joins)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
aliases[initial_table] = 1
|
25
|
+
new(connection, aliases)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.initial_count_for(connection, name, table_joins)
|
29
|
+
quoted_name = nil
|
30
|
+
|
31
|
+
counts = table_joins.map do |join|
|
32
|
+
if join.is_a?(Arel::Nodes::StringJoin)
|
33
|
+
# quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
|
34
|
+
quoted_name ||= connection.quote_table_name(name)
|
35
|
+
|
36
|
+
# Table names + table aliases
|
37
|
+
join.left.scan(
|
38
|
+
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
|
39
|
+
).size
|
40
|
+
elsif join.is_a?(Arel::Nodes::Join)
|
41
|
+
join.left.name == name ? 1 : 0
|
42
|
+
else
|
43
|
+
raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
counts.sum
|
48
|
+
end
|
49
|
+
|
50
|
+
# table_joins is an array of arel joins which might conflict with the aliases we assign here
|
51
|
+
def initialize(connection, aliases)
|
52
|
+
@aliases = aliases
|
53
|
+
@connection = connection
|
54
|
+
end
|
55
|
+
|
56
|
+
def aliased_table_for(table_name, aliased_name, type_caster)
|
57
|
+
if aliases[table_name].zero?
|
58
|
+
# If it's zero, we can have our table_name
|
59
|
+
aliases[table_name] = 1
|
60
|
+
Arel::Table.new(table_name, type_caster: type_caster)
|
61
|
+
else
|
62
|
+
# Otherwise, we need to use an alias
|
63
|
+
aliased_name = @connection.table_alias_for(aliased_name)
|
64
|
+
|
65
|
+
# Update the count
|
66
|
+
aliases[aliased_name] += 1
|
67
|
+
|
68
|
+
table_alias = if aliases[aliased_name] > 1
|
69
|
+
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
|
70
|
+
else
|
71
|
+
aliased_name
|
72
|
+
end
|
73
|
+
Arel::Table.new(table_name, type_caster: type_caster).alias(table_alias)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :aliases
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def truncate(name)
|
82
|
+
name.slice(0, @connection.table_alias_length - 2)
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# The core logic of this class is old Rails behaviour, replicated here because
|
2
|
+
# their own alias logic has evolved, but I haven't yet found a way to make use
|
3
|
+
# of it - and besides, this is only used to generate Thinking Sphinx's
|
4
|
+
# configuration rarely - not in any web requests, so performance issues are less
|
5
|
+
# critical here.
|
6
|
+
|
7
|
+
class Joiner::JoinAliaser
|
8
|
+
def self.call(join_root, alias_tracker)
|
9
|
+
new(join_root, alias_tracker).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(join_root, alias_tracker)
|
13
|
+
@join_root = join_root
|
14
|
+
@alias_tracker = alias_tracker
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
join_root.each_children do |parent, child|
|
19
|
+
child.table = table_aliases_for(parent, child).first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :join_root, :alias_tracker
|
26
|
+
|
27
|
+
def table_aliases_for(parent, node)
|
28
|
+
node.reflection.chain.map { |reflection|
|
29
|
+
alias_tracker.aliased_table_for(
|
30
|
+
reflection.table_name,
|
31
|
+
table_alias_for(reflection, parent, reflection != node.reflection),
|
32
|
+
reflection.klass.type_caster
|
33
|
+
)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def table_alias_for(reflection, parent, join)
|
38
|
+
name = reflection.alias_candidate(parent.table_name)
|
39
|
+
join ? "#{name}_join" : name
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Joiner::JoinDependency < ActiveRecord::Associations::JoinDependency
|
2
|
+
def join_association_for(path, alias_tracker = nil)
|
3
|
+
@alias_tracker = alias_tracker
|
4
|
+
|
5
|
+
Joiner::JoinAliaser.call join_root, alias_tracker
|
6
|
+
|
7
|
+
path.inject(join_root) do |node, piece|
|
8
|
+
node.children.detect { |child| child.reflection.name == piece }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/joiner/joins.rb
CHANGED
@@ -2,11 +2,7 @@ require 'active_record'
|
|
2
2
|
require 'active_support/ordered_hash'
|
3
3
|
|
4
4
|
class Joiner::Joins
|
5
|
-
|
6
|
-
JoinAssociation = JoinDependency::JoinAssociation
|
7
|
-
|
8
|
-
attr_reader :model
|
9
|
-
attr_accessor :join_association_class
|
5
|
+
attr_reader :model
|
10
6
|
|
11
7
|
def initialize(model)
|
12
8
|
@model = model
|
@@ -23,35 +19,34 @@ class Joiner::Joins
|
|
23
19
|
return model.table_name if path.empty?
|
24
20
|
|
25
21
|
add_join_to path
|
26
|
-
|
22
|
+
association_for(path).table.name
|
27
23
|
end
|
28
24
|
|
29
25
|
def join_values
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
result
|
26
|
+
Joiner::JoinDependency.new(
|
27
|
+
model, table, joins_cache.to_a, Arel::Nodes::OuterJoin
|
28
|
+
)
|
35
29
|
end
|
36
30
|
|
37
31
|
private
|
38
32
|
|
39
33
|
attr_reader :joins_cache
|
40
34
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
|
35
|
+
def alias_tracker
|
36
|
+
Joiner::AliasTracker.create(
|
37
|
+
model.connection, table.name, []
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def association_for(path)
|
42
|
+
join_values.join_association_for path, alias_tracker
|
45
43
|
end
|
46
44
|
|
47
45
|
def path_as_hash(path)
|
48
46
|
path[0..-2].reverse.inject(path.last) { |key, item| {item => key} }
|
49
47
|
end
|
50
48
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
JoinDependency.send :remove_const, :JoinAssociation
|
55
|
-
JoinDependency.const_set :JoinAssociation, klass
|
49
|
+
def table
|
50
|
+
@table ||= model.arel_table
|
56
51
|
end
|
57
52
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: joiner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pat Allan
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-12-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,70 +16,70 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: combustion
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '1.1'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: '1.1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rails
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 6.1.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 6.1.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rspec-rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: '4'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: '4'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: sqlite3
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 1.
|
75
|
+
version: '1.4'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 1.
|
82
|
+
version: '1.4'
|
83
83
|
description: Builds ActiveRecord outer joins from association paths and provides references
|
84
84
|
to table aliases.
|
85
85
|
email:
|
@@ -88,13 +88,18 @@ executables: []
|
|
88
88
|
extensions: []
|
89
89
|
extra_rdoc_files: []
|
90
90
|
files:
|
91
|
+
- ".github/workflows/ci.yml"
|
91
92
|
- ".gitignore"
|
93
|
+
- ".travis.yml"
|
92
94
|
- Gemfile
|
93
95
|
- LICENSE.txt
|
94
96
|
- README.md
|
95
97
|
- Rakefile
|
96
98
|
- joiner.gemspec
|
97
99
|
- lib/joiner.rb
|
100
|
+
- lib/joiner/alias_tracker.rb
|
101
|
+
- lib/joiner/join_aliaser.rb
|
102
|
+
- lib/joiner/join_dependency.rb
|
98
103
|
- lib/joiner/joins.rb
|
99
104
|
- lib/joiner/path.rb
|
100
105
|
- spec/acceptance/joiner_spec.rb
|
@@ -110,7 +115,7 @@ homepage: https://github.com/pat/joiner
|
|
110
115
|
licenses:
|
111
116
|
- MIT
|
112
117
|
metadata: {}
|
113
|
-
post_install_message:
|
118
|
+
post_install_message:
|
114
119
|
rdoc_options: []
|
115
120
|
require_paths:
|
116
121
|
- lib
|
@@ -125,9 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
130
|
- !ruby/object:Gem::Version
|
126
131
|
version: '0'
|
127
132
|
requirements: []
|
128
|
-
|
129
|
-
|
130
|
-
signing_key:
|
133
|
+
rubygems_version: 3.1.2
|
134
|
+
signing_key:
|
131
135
|
specification_version: 4
|
132
136
|
summary: Builds ActiveRecord joins from association paths
|
133
137
|
test_files:
|