joiner 0.3.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7249d05e24afee39fbb2547085068230f6b1d62b
4
- data.tar.gz: 941f72f5d80c3a6c0b8b88684f7a35b77e7461ee
2
+ SHA256:
3
+ metadata.gz: 50d83b6e1af0b5ef8c29fbbaabbb56a8c33ed99acbf57f507d13bcff673d92b2
4
+ data.tar.gz: 62adfb53520243ff108e85f68c61ca121cf28f1e6344fac539aa36c725efe0c1
5
5
  SHA512:
6
- metadata.gz: 0fd9651d510164f7e5843589678ba62d3f214c67dca6309e4711f71cd8b4200b3e105cd780fdfab14d3a4240d4354fcd95b6568b101d710757da1d093e96600a
7
- data.tar.gz: ff41eaace4916f013c03e7407a65709543251908daab4f3203f723282a9c38c50a35b9767a9b074bb6f7f336bb1042cf30542d8ee179cd08ca868a23228b66fe
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"
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ dist: xenial
3
+ sudo: false
4
+ rvm:
5
+ - 2.5.8
6
+ - 2.6.6
7
+ before_install:
8
+ - gem update --system
9
+ script: bundle exec rspec
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-4.0 compared to v4.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.3.4
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 user
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
@@ -1 +1,6 @@
1
1
  require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
@@ -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.3.4'
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', '>= 4.1.0'
16
+ spec.add_runtime_dependency 'activerecord', '>= 6.1.0'
17
17
 
18
- spec.add_development_dependency 'combustion', '~> 0.5.1'
19
- spec.add_development_dependency 'rails', '>= 4.1.2'
20
- spec.add_development_dependency 'rspec-rails', '~> 2.14.1'
21
- spec.add_development_dependency 'sqlite3', '~> 1.3.8'
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
@@ -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
@@ -2,11 +2,7 @@ require 'active_record'
2
2
  require 'active_support/ordered_hash'
3
3
 
4
4
  class Joiner::Joins
5
- JoinDependency = ActiveRecord::Associations::JoinDependency
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
- join_association_for(path).tables.first.name
22
+ association_for(path).table.name
27
23
  end
28
24
 
29
25
  def join_values
30
- switch_join_dependency join_association_class
31
- result = JoinDependency.new model, joins_cache.to_a, []
32
- switch_join_dependency JoinAssociation
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 join_association_for(path)
42
- path.inject(join_values.join_root) do |node, piece|
43
- node.children.detect { |child| child.reflection.name == piece }
44
- end
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 switch_join_dependency(klass)
52
- return unless join_association_class
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
@@ -3,7 +3,7 @@ require 'bundler/setup'
3
3
 
4
4
  require 'combustion'
5
5
 
6
- Combustion.initialize! :all
6
+ Combustion.initialize! :active_record
7
7
 
8
8
  require 'rspec/rails'
9
9
 
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.3.4
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: 2014-11-17 00:00:00.000000000 Z
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: 4.1.0
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: 4.1.0
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: 0.5.1
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: 0.5.1
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: 4.1.2
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: 4.1.2
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: 2.14.1
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: 2.14.1
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.3.8
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.3.8
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
- rubyforge_project:
129
- rubygems_version: 2.3.0
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: