activerecord-precount 0.4.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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +28 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE +22 -0
  6. data/README.md +91 -0
  7. data/Rakefile +14 -0
  8. data/activerecord-precount.gemspec +31 -0
  9. data/benchmark.rb +42 -0
  10. data/ci/Gemfile.activerecord-4.1.x +4 -0
  11. data/ci/Gemfile.activerecord-4.2.x +4 -0
  12. data/ci/travis.rb +14 -0
  13. data/lib/active_record/associations/builder/count_loader.rb +13 -0
  14. data/lib/active_record/associations/count_loader.rb +27 -0
  15. data/lib/active_record/associations/preloader/count_loader.rb +41 -0
  16. data/lib/active_record/precount/base_extension.rb +7 -0
  17. data/lib/active_record/precount/extend.rb +18 -0
  18. data/lib/active_record/precount/has_many_extension.rb +25 -0
  19. data/lib/active_record/precount/join_dependency_extension.rb +23 -0
  20. data/lib/active_record/precount/preloader_extension.rb +15 -0
  21. data/lib/active_record/precount/reflection_extension.rb +50 -0
  22. data/lib/active_record/precount/relation_extension.rb +42 -0
  23. data/lib/active_record/precount/version.rb +5 -0
  24. data/lib/activerecord-precount.rb +8 -0
  25. data/sample/.gitignore +16 -0
  26. data/sample/Gemfile +19 -0
  27. data/sample/README.md +6 -0
  28. data/sample/Rakefile +6 -0
  29. data/sample/app/assets/images/.keep +0 -0
  30. data/sample/app/assets/javascripts/application.js +16 -0
  31. data/sample/app/assets/stylesheets/application.css +15 -0
  32. data/sample/app/controllers/application_controller.rb +13 -0
  33. data/sample/app/controllers/concerns/.keep +0 -0
  34. data/sample/app/helpers/application_helper.rb +2 -0
  35. data/sample/app/mailers/.keep +0 -0
  36. data/sample/app/models/.keep +0 -0
  37. data/sample/app/models/concerns/.keep +0 -0
  38. data/sample/app/models/favorite.rb +4 -0
  39. data/sample/app/models/tweet.rb +6 -0
  40. data/sample/app/models/user.rb +4 -0
  41. data/sample/app/views/application/index.html.erb +49 -0
  42. data/sample/app/views/layouts/application.html.erb +14 -0
  43. data/sample/bin/bundle +3 -0
  44. data/sample/bin/rails +8 -0
  45. data/sample/bin/rake +8 -0
  46. data/sample/bin/spring +18 -0
  47. data/sample/config.ru +4 -0
  48. data/sample/config/application.rb +30 -0
  49. data/sample/config/boot.rb +4 -0
  50. data/sample/config/database.yml +11 -0
  51. data/sample/config/environment.rb +5 -0
  52. data/sample/config/environments/development.rb +40 -0
  53. data/sample/config/environments/production.rb +82 -0
  54. data/sample/config/environments/test.rb +39 -0
  55. data/sample/config/initializers/assets.rb +8 -0
  56. data/sample/config/initializers/backtrace_silencers.rb +7 -0
  57. data/sample/config/initializers/cookies_serializer.rb +3 -0
  58. data/sample/config/initializers/filter_parameter_logging.rb +4 -0
  59. data/sample/config/initializers/inflections.rb +16 -0
  60. data/sample/config/initializers/mime_types.rb +4 -0
  61. data/sample/config/initializers/session_store.rb +3 -0
  62. data/sample/config/initializers/wrap_parameters.rb +14 -0
  63. data/sample/config/locales/en.yml +23 -0
  64. data/sample/config/routes.rb +3 -0
  65. data/sample/config/secrets.yml +22 -0
  66. data/sample/db/migrate/20141122002518_create_tweets.rb +10 -0
  67. data/sample/db/migrate/20141122002548_create_favorites.rb +10 -0
  68. data/sample/db/migrate/20141122002555_create_users.rb +8 -0
  69. data/sample/db/schema.rb +35 -0
  70. data/sample/db/seeds.rb +12 -0
  71. data/sample/lib/assets/.keep +0 -0
  72. data/sample/lib/tasks/.keep +0 -0
  73. data/sample/log/.keep +0 -0
  74. data/sample/public/404.html +67 -0
  75. data/sample/public/422.html +67 -0
  76. data/sample/public/500.html +66 -0
  77. data/sample/public/favicon.ico +0 -0
  78. data/sample/public/robots.txt +5 -0
  79. data/sample/vendor/assets/javascripts/.keep +0 -0
  80. data/sample/vendor/assets/stylesheets/.keep +0 -0
  81. data/test/cases/associations/eager_load_test.rb +20 -0
  82. data/test/cases/associations/includes_test.rb +30 -0
  83. data/test/cases/associations/precount_test.rb +36 -0
  84. data/test/cases/associations/preload_test.rb +30 -0
  85. data/test/cases/db_config.rb +24 -0
  86. data/test/cases/helper.rb +4 -0
  87. data/test/cases/test_case.rb +86 -0
  88. data/test/config.example.yml +140 -0
  89. data/test/config.rb +5 -0
  90. data/test/models/favorite.rb +3 -0
  91. data/test/models/tweet.rb +4 -0
  92. data/test/schema/schema.rb +16 -0
  93. data/test/support/autorun.rb +5 -0
  94. data/test/support/config.rb +43 -0
  95. data/test/support/connection.rb +15 -0
  96. metadata +293 -0
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
@@ -0,0 +1,5 @@
1
+ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2
+ #
3
+ # To ban all spiders from the entire site uncomment the next two lines:
4
+ # User-agent: *
5
+ # Disallow: /
File without changes
File without changes
@@ -0,0 +1,20 @@
1
+ require 'cases/helper'
2
+ require 'models/favorite'
3
+ require 'models/tweet'
4
+
5
+ class EagerLoadTest < ActiveRecord::CountLoader::TestCase
6
+ def setup
7
+ tweet = Tweet.create
8
+ Favorite.create(tweet: tweet)
9
+ end
10
+
11
+ def teardown
12
+ [Tweet, Favorite].each(&:delete_all)
13
+ end
14
+
15
+ def test_eager_loaded_count_loader_raises_an_error
16
+ assert_raises ActiveRecord::EagerLoadCountLoaderError do
17
+ Tweet.eager_load(:favorites_count).to_a
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require 'cases/helper'
2
+
3
+ class IncludesTest < ActiveRecord::CountLoader::TestCase
4
+ def setup
5
+ tweets_count.times.map do |index|
6
+ tweet = Tweet.create
7
+ index.times { Favorite.create(tweet: tweet) }
8
+ end
9
+ end
10
+
11
+ def teardown
12
+ [Tweet, Favorite].each(&:delete_all)
13
+ end
14
+
15
+ def tweets_count
16
+ 3
17
+ end
18
+
19
+ def test_includes_does_not_execute_n_1_queries
20
+ assert_queries(1 + tweets_count) { Tweet.all.map { |t| t.favorites.count } }
21
+ assert_queries(1 + tweets_count) { Tweet.all.map(&:favorites_count) }
22
+ assert_queries(2) { Tweet.includes(:favorites_count).map(&:favorites_count) }
23
+ end
24
+
25
+ def test_included_count_loader_counts_properly
26
+ expected = Tweet.all.map { |t| t.favorites.count }
27
+ assert_equal(Tweet.all.map(&:favorites_count), expected)
28
+ assert_equal(Tweet.includes(:favorites_count).map(&:favorites_count), expected)
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ require 'cases/helper'
2
+
3
+ class PrecountTest < ActiveRecord::CountLoader::TestCase
4
+ def setup
5
+ tweets_count.times.map do |index|
6
+ tweet = Tweet.create
7
+ index.times { Favorite.create(tweet: tweet) }
8
+ end
9
+ end
10
+
11
+ def teardown
12
+ [Tweet, Favorite].each(&:delete_all)
13
+ end
14
+
15
+ def tweets_count
16
+ 3
17
+ end
18
+
19
+ def test_precount_has_many_does_not_execute_n_1_queries
20
+ assert_equal(Tweet.reflections['favs_count'].present?, false)
21
+ assert_queries(1 + tweets_count) { Tweet.all.map { |t| t.favs.count } }
22
+ assert_queries(2) { Tweet.precount(:favs).map(&:favs_count) }
23
+ end
24
+
25
+ def test_precount_has_many_with_count_loader_does_not_execute_n_1_queries
26
+ assert_queries(1 + tweets_count) { Tweet.all.map { |t| t.favorites.count } }
27
+ assert_queries(1 + tweets_count) { Tweet.all.map(&:favorites_count) }
28
+ assert_queries(2) { Tweet.precount(:favorites).map(&:favorites_count) }
29
+ end
30
+
31
+ def test_precount_has_many_counts_properly
32
+ expected = Tweet.all.map { |t| t.favorites.count }
33
+ assert_equal(Tweet.all.map(&:favorites_count), expected)
34
+ assert_equal(Tweet.precount(:favorites).map(&:favorites_count), expected)
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ require 'cases/helper'
2
+
3
+ class PreloadTest < ActiveRecord::CountLoader::TestCase
4
+ def setup
5
+ tweets_count.times.map do |index|
6
+ tweet = Tweet.create
7
+ index.times { Favorite.create(tweet: tweet) }
8
+ end
9
+ end
10
+
11
+ def teardown
12
+ [Tweet, Favorite].each(&:delete_all)
13
+ end
14
+
15
+ def tweets_count
16
+ 3
17
+ end
18
+
19
+ def test_preload_does_not_execute_n_1_queries
20
+ assert_queries(1 + tweets_count) { Tweet.all.map { |t| t.favorites.count } }
21
+ assert_queries(1 + tweets_count) { Tweet.all.map(&:favorites_count) }
22
+ assert_queries(2) { Tweet.preload(:favorites_count).map(&:favorites_count) }
23
+ end
24
+
25
+ def test_preloaded_count_loader_counts_properly
26
+ expected = Tweet.all.map { |t| t.favorites.count }
27
+ assert_equal(Tweet.all.map(&:favorites_count), expected)
28
+ assert_equal(Tweet.preload(:favorites_count).map(&:favorites_count), expected)
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ require 'config'
2
+
3
+ require 'active_record'
4
+ require 'activerecord-precount'
5
+
6
+ require 'support/config'
7
+ require 'support/connection'
8
+
9
+ # Connect to the database
10
+ ARTest.connect
11
+
12
+ def load_schema
13
+ # silence verbose schema loading
14
+ original_stdout = $stdout
15
+ $stdout = StringIO.new
16
+
17
+ adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
18
+
19
+ load SCHEMA_ROOT + "/schema.rb"
20
+ ensure
21
+ $stdout = original_stdout
22
+ end
23
+
24
+ load_schema
@@ -0,0 +1,4 @@
1
+ require 'cases/db_config'
2
+
3
+ require 'support/autorun'
4
+ require 'cases/test_case'
@@ -0,0 +1,86 @@
1
+ module ActiveRecord::CountLoader
2
+ class TestCase < Minitest::Test
3
+ def teardown
4
+ SQLCounter.clear_log
5
+ end
6
+
7
+ def capture_sql
8
+ SQLCounter.clear_log
9
+ yield
10
+ SQLCounter.log_all.dup
11
+ end
12
+
13
+ def assert_sql(*patterns_to_match)
14
+ SQLCounter.clear_log
15
+ yield
16
+ SQLCounter.log_all
17
+ ensure
18
+ failed_patterns = []
19
+ patterns_to_match.each do |pattern|
20
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
21
+ end
22
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
23
+ end
24
+
25
+ def assert_queries(num = 1, options = {})
26
+ ignore_none = options.fetch(:ignore_none) { num == :any }
27
+ SQLCounter.clear_log
28
+ x = yield
29
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
30
+ if num == :any
31
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
32
+ else
33
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
34
+ assert_equal num, the_log.size, mesg
35
+ end
36
+ x
37
+ end
38
+
39
+ def assert_no_queries(options = {}, &block)
40
+ options.reverse_merge! ignore_none: true
41
+ assert_queries(0, options, &block)
42
+ end
43
+ end
44
+
45
+ class SQLCounter
46
+ class << self
47
+ attr_accessor :ignored_sql, :log, :log_all
48
+ def clear_log; self.log = []; self.log_all = []; end
49
+ end
50
+
51
+ self.clear_log
52
+
53
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
54
+
55
+ # FIXME: this needs to be refactored so specific database can add their own
56
+ # ignored SQL, or better yet, use a different notification for the queries
57
+ # instead examining the SQL content.
58
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
59
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
60
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
61
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
62
+
63
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
64
+ ignored_sql.concat db_ignored_sql
65
+ end
66
+
67
+ attr_reader :ignore
68
+
69
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
70
+ @ignore = ignore
71
+ end
72
+
73
+ def call(name, start, finish, message_id, values)
74
+ sql = values[:sql]
75
+
76
+ # FIXME: this seems bad. we should probably have a better way to indicate
77
+ # the query was cached
78
+ return if 'CACHE' == values[:name]
79
+
80
+ self.class.log_all << sql
81
+ self.class.log << sql unless ignore =~ sql
82
+ end
83
+ end
84
+
85
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
86
+ end
@@ -0,0 +1,140 @@
1
+ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
2
+
3
+ with_manual_interventions: false
4
+
5
+ connections:
6
+ jdbcderby:
7
+ arunit: activerecord_unittest
8
+ arunit2: activerecord_unittest2
9
+
10
+ jdbch2:
11
+ arunit: activerecord_unittest
12
+ arunit2: activerecord_unittest2
13
+
14
+ jdbchsqldb:
15
+ arunit: activerecord_unittest
16
+ arunit2: activerecord_unittest2
17
+
18
+ jdbcmysql:
19
+ arunit:
20
+ username: rails
21
+ encoding: utf8
22
+ arunit2:
23
+ username: rails
24
+ encoding: utf8
25
+
26
+ jdbcpostgresql:
27
+ arunit:
28
+ username: <%= ENV['user'] || 'rails' %>
29
+ arunit2:
30
+ username: <%= ENV['user'] || 'rails' %>
31
+
32
+ jdbcsqlite3:
33
+ arunit:
34
+ database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
35
+ timeout: 5000
36
+ arunit2:
37
+ database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
38
+ timeout: 5000
39
+
40
+ db2:
41
+ arunit:
42
+ adapter: ibm_db
43
+ host: localhost
44
+ username: arunit
45
+ password: arunit
46
+ database: arunit
47
+ arunit2:
48
+ adapter: ibm_db
49
+ host: localhost
50
+ username: arunit
51
+ password: arunit
52
+ database: arunit2
53
+
54
+ firebird:
55
+ arunit:
56
+ host: localhost
57
+ username: rails
58
+ password: rails
59
+ charset: UTF8
60
+ arunit2:
61
+ host: localhost
62
+ username: rails
63
+ password: rails
64
+ charset: UTF8
65
+
66
+ frontbase:
67
+ arunit:
68
+ host: localhost
69
+ username: rails
70
+ session_name: unittest-<%= $$ %>
71
+ arunit2:
72
+ host: localhost
73
+ username: rails
74
+ session_name: unittest-<%= $$ %>
75
+
76
+ mysql:
77
+ arunit:
78
+ username: rails
79
+ encoding: utf8
80
+ arunit2:
81
+ username: rails
82
+ encoding: utf8
83
+
84
+ mysql2:
85
+ arunit:
86
+ username: rails
87
+ encoding: utf8
88
+ arunit2:
89
+ username: rails
90
+ encoding: utf8
91
+
92
+ openbase:
93
+ arunit:
94
+ username: admin
95
+ arunit2:
96
+ username: admin
97
+
98
+ oracle:
99
+ arunit:
100
+ adapter: oracle_enhanced
101
+ database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %>
102
+ username: <%= ENV['ARUNIT_USER_NAME'] || 'arunit' %>
103
+ password: <%= ENV['ARUNIT_PASSWORD'] || 'arunit' %>
104
+ emulate_oracle_adapter: true
105
+ arunit2:
106
+ adapter: oracle_enhanced
107
+ database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %>
108
+ username: <%= ENV['ARUNIT2_USER_NAME'] || 'arunit2' %>
109
+ password: <%= ENV['ARUNIT2_PASSWORD'] || 'arunit2' %>
110
+ emulate_oracle_adapter: true
111
+
112
+ postgresql:
113
+ arunit:
114
+ min_messages: warning
115
+ arunit2:
116
+ min_messages: warning
117
+
118
+ sqlite3:
119
+ arunit:
120
+ database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
121
+ timeout: 5000
122
+ arunit2:
123
+ database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
124
+ timeout: 5000
125
+
126
+ sqlite3_mem:
127
+ arunit:
128
+ adapter: sqlite3
129
+ database: ':memory:'
130
+ arunit2:
131
+ adapter: sqlite3
132
+ database: ':memory:'
133
+
134
+ sybase:
135
+ arunit:
136
+ host: database_ASE
137
+ username: sa
138
+ arunit2:
139
+ host: database_ASE
140
+ username: sa