activerecord-postgresql-cursors 1.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,26 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2024-08-06 02:41:08 UTC using RuboCop version 1.65.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
11
+ Metrics/AbcSize:
12
+ Exclude:
13
+ - 'lib/activerecord-postgresql-cursors.rb'
14
+
15
+ # Offense count: 1
16
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
17
+ Metrics/PerceivedComplexity:
18
+ Max: 9
19
+
20
+ # Offense count: 1
21
+ # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
22
+ # CheckDefinitionPathHierarchyRoots: lib, spec, test, src
23
+ # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
24
+ Naming/FileName:
25
+ Exclude:
26
+ - 'lib/activerecord-postgresql-cursors.rb'
data/FUNDING.yml ADDED
@@ -0,0 +1,2 @@
1
+ github:
2
+ - dark-panda
data/Gemfile CHANGED
@@ -1,21 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
4
6
 
5
- if RUBY_PLATFORM == "java"
6
- gem "activerecord-jdbcpostgresql-adapter"
7
- else
8
- gem "pg"
9
- end
10
-
11
- gem "rdoc", "~> 3.12"
12
- gem "rake", "~> 10.0"
13
- gem "minitest", "~> 4.7"
14
- gem "minitest-reporters"
15
- gem "guard-minitest"
16
- gem "simplecov"
17
-
18
- if File.exists?('Gemfile.local')
19
- instance_eval File.read('Gemfile.local')
20
- end
7
+ gem 'guard'
8
+ gem 'guard-minitest'
9
+ gem 'minitest'
10
+ gem 'minitest-reporters'
11
+ gem 'pg'
12
+ gem 'rake'
13
+ gem 'rubocop', require: false
14
+ gem 'rubocop-minitest', require: false
15
+ gem 'simplecov', require: false
16
+ gem 'simplecov_json_formatter', require: false
21
17
 
18
+ instance_eval File.read('Gemfile.local') if File.exist?('Gemfile.local')
data/Guardfile CHANGED
@@ -1,17 +1,15 @@
1
+ # frozen_string_literal: true
1
2
 
2
- guard 'minitest', :test_folders => 'test', :test_file_patterns => '*_tests.rb' do
3
- watch(%r|^test/(.+)_tests\.rb|)
3
+ guard 'minitest', test_folders: 'test', test_file_patterns: '*_tests.rb' do
4
+ watch(%r{^test/(.+)_tests\.rb})
4
5
 
5
- watch(%r|^lib/(.*)([^/]+)\.rb|) do |m|
6
- "test/cursor_tests.rb"
6
+ watch(%r{^lib/(.*)([^/]+)\.rb}) do |_m|
7
+ 'test/cursor_tests.rb'
7
8
  end
8
9
 
9
- watch(%r|^test/test_helper\.rb|) do
10
- "test"
10
+ watch(%r{^test/test_helper\.rb}) do
11
+ 'test'
11
12
  end
12
13
  end
13
14
 
14
- if File.exists?('Guardfile.local')
15
- instance_eval File.read('Guardfile.local')
16
- end
17
-
15
+ instance_eval File.read('Guardfile.local') if File.exist?('Guardfile.local')
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008-2016 J Smith <dark.panda@gmail.com>
1
+ Copyright (c) 2008-2024 J Smith <dark.panda@gmail.com>
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
@@ -20,4 +20,3 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
20
  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
21
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
22
  OTHER DEALINGS IN THE SOFTWARE.
23
-
@@ -1,4 +1,4 @@
1
- == ActiveRecord PostgreSQL Cursors
1
+ # ActiveRecord PostgreSQL Cursors
2
2
 
3
3
  This extension allows you to loop through record sets using cursors in an
4
4
  Enumerable fashion. This allows you to cut down memory usage by only pulling
@@ -9,21 +9,24 @@ To use a cursor, just change the first parameter to an
9
9
  ActiveRecord::Base.find to :cursor instead of :first or :all or
10
10
  whatever or use the ActiveRecord::Base.cursor method directly.
11
11
 
12
- MyModel.find(:cursor, :conditions => 'some_column = true').each do |r|
13
- puts r.inspect
14
- end
12
+ ```ruby
13
+ MyModel.find(:cursor, :conditions => 'some_column = true').each do |r|
14
+ puts r.inspect
15
+ end
15
16
 
16
- MyModel.find(:cursor).collect { |r| r.foo / PI }.avg
17
+ MyModel.find(:cursor).collect { |r| r.foo / PI }.avg
17
18
 
18
- MyModel.cursor.each do |r|
19
- puts r.inspect
20
- end
19
+ MyModel.cursor.each do |r|
20
+ puts r.inspect
21
+ end
22
+ ```
21
23
 
22
24
  All ActiveRecord::Base.find options are available and should work as-is.
23
25
  As a bonus, the PostgreSQLCursor object returned includes Enumerable,
24
26
  so you can iterate to your heart's content.
25
27
 
26
- This extension should work in both Rails 2.3 as well as Rails 3.
28
+ This extension should work with Rails 6.1+. For older versions of Rails, try
29
+ out older versions of the gem.
27
30
 
28
31
  At the moment, this is a non-scrollable cursor -- it will only fetch
29
32
  forward. Also note that these cursors are non-updateable/insensitive to
@@ -48,5 +51,9 @@ while the first query is just used to build the larger joined query. This
48
51
  allows for a brief window between the point that the cursor query is
49
52
  created and the time it is executed. In these cases, it may be wise to wrap
50
53
  your use or cursors in your own transaction to ensure that changes made to
51
- the underlying data don't interfere with your cursor's visbility.
54
+ the underlying data don't interfere with your cursor's visibility.
52
55
 
56
+ ## License
57
+
58
+ This gem is licensed under an MIT-style license. See the +MIT-LICENSE+ file for
59
+ details.
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
-
2
- # -*- ruby -*-
1
+ # frozen_string_literal: true
3
2
 
4
3
  require 'rubygems'
5
4
  require 'rubygems/package_task'
@@ -7,22 +6,24 @@ require 'rake/testtask'
7
6
  require 'rdoc/task'
8
7
  require 'bundler/gem_tasks'
9
8
 
10
- $:.push File.expand_path(File.dirname(__FILE__), 'lib')
9
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__), 'lib')
11
10
 
12
11
  version = ActiveRecord::PostgreSQLCursors::VERSION
13
12
 
14
13
  desc 'Test PostgreSQL extensions'
15
14
  Rake::TestTask.new(:test) do |t|
16
- t.test_files = FileList['test/**/*_tests.rb']
15
+ t.libs << "#{File.dirname(__FILE__)}/test"
16
+ t.test_files = FileList['test/**/*_test.rb']
17
17
  t.verbose = !!ENV['VERBOSE_TESTS']
18
18
  t.warning = !!ENV['WARNINGS']
19
19
  end
20
20
 
21
+ task default: :test
22
+
21
23
  desc 'Build docs'
22
24
  Rake::RDocTask.new do |t|
23
25
  t.title = "ActiveRecord PostgreSQL Cursors #{version}"
24
- t.main = 'README.rdoc'
26
+ t.main = 'README.md'
25
27
  t.rdoc_dir = 'doc'
26
28
  t.rdoc_files.include('README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb')
27
29
  end
28
-
@@ -1,26 +1,26 @@
1
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- require File.expand_path('../lib/active_record/postgresql_cursors/version', __FILE__)
3
+ require File.expand_path('lib/active_record/postgresql_cursors/version', __dir__)
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "activerecord-postgresql-cursors"
6
+ s.name = 'activerecord-postgresql-cursors'
7
7
  s.version = ActiveRecord::PostgreSQLCursors::VERSION
8
8
 
9
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
- s.authors = ["J Smith"]
11
- s.description = "Provides some support for PostgreSQL cursors in ActiveRecord."
9
+ s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
10
+ s.required_ruby_version = '>= 3.0'
11
+ s.authors = ['J Smith']
12
+ s.description = 'Provides some support for PostgreSQL cursors in ActiveRecord.'
12
13
  s.summary = s.description
13
- s.email = "dark.panda@gmail.com"
14
- s.license = "MIT"
14
+ s.email = 'dark.panda@gmail.com'
15
+ s.license = 'MIT'
15
16
  s.extra_rdoc_files = [
16
- "README.rdoc"
17
+ 'README.md'
17
18
  ]
18
- s.files = `git ls-files`.split($\)
19
+ s.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
19
20
  s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
21
- s.homepage = "http://github.com/dark-panda/activerecord-postgresql-cursors"
22
- s.require_paths = ["lib"]
21
+ s.homepage = 'http://github.com/dark-panda/activerecord-postgresql-cursors'
22
+ s.require_paths = ['lib']
23
23
 
24
- s.add_dependency("activerecord", [">= 2.3"])
24
+ s.add_dependency('activerecord', ['>= 6.1'])
25
+ s.metadata['rubygems_mfa_required'] = 'true'
25
26
  end
26
-
@@ -1,15 +1,10 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module CursorExtensions
4
5
  extend ActiveSupport::Concern
5
6
 
6
- included do
7
- alias_method_chain :find, :cursors
8
- end
9
-
10
- # Override ActiveRecord::Base#find to allow for cursors in
11
- # PostgreSQL. To use a cursor, set the first argument of
12
- # find to :cursor. A PostgreSQLCursor object will be returned,
7
+ # Find using cursors. A PostgreSQLCursor object will be returned,
13
8
  # which can then be used as an Enumerable to loop through the
14
9
  # results.
15
10
  #
@@ -18,45 +13,30 @@ module ActiveRecord
18
13
  # is pretty unlikely to clash if you're using nested cursors.
19
14
  # Alternatively, you can supply a specific cursor name by
20
15
  # supplying a :cursor_name option.
21
- def find_with_cursors(*args)
22
- if args.first.to_s == 'cursor'
16
+ def cursor(*args)
17
+ find_with_cursor('cursor', *args)
18
+ end
19
+
20
+ private
21
+
22
+ def find_with_cursor(*args)
23
23
  options = args.extract_options!
24
24
  cursor_name = options.delete(:cursor_name)
25
25
  find_cursor(cursor_name, options)
26
- else
27
- find_without_cursors(*args)
28
26
  end
29
- end
30
-
31
- def cursor(*args)
32
- find_with_cursors('cursor', *args)
33
- end
34
27
 
35
- private
36
28
  # Find method for using cursors. This works just like the regular
37
29
  # ActiveRecord::Base#find_every method, except it returns a
38
30
  # PostgreSQLCursor object that can be used to loop through records.
39
31
  def find_cursor(cursor_name, options)
40
- unless connection.is_a? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
41
- raise CursorsNotSupported, "#{connection.class} doesn't support cursors"
42
- end
43
-
44
- relation = if ActiveRecord::VERSION::MAJOR >= 4
45
- apply_finder_options(options, silence_deprecation = true)
46
- else
47
- apply_finder_options(options)
48
- end
32
+ raise CursorsNotSupported, "#{connection.class} doesn't support cursors" unless connection.is_a? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
49
33
 
34
+ relation = merge(options.slice(:readonly, :references, :order, :limit, :joins, :group, :having, :offset, :select, :uniq))
50
35
  including = (relation.eager_load_values + relation.includes_values).uniq
51
36
 
52
37
  if including.present?
53
- join_dependency = if ActiveRecord::VERSION::STRING >= '3.1'
54
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
55
- else
56
- ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
57
- end
58
-
59
- join_relation = relation.construct_relation_for_association_find(join_dependency)
38
+ join_dependency = construct_join_dependency(joins_values, nil)
39
+ join_relation = apply_join_dependency
60
40
 
61
41
  ActiveRecord::PostgreSQLCursor.new(self, cursor_name, join_relation, join_dependency)
62
42
  else
@@ -64,23 +44,6 @@ module ActiveRecord
64
44
  end
65
45
  end
66
46
  end
67
-
68
- class PostgreSQLCursor
69
- def initialize_with_rails(model, cursor_name, relation, join_dependency = nil)
70
- @relation = relation
71
-
72
- query = if ActiveRecord::VERSION::MAJOR >= 4
73
- model.connection.unprepared_statement do
74
- relation.to_sql
75
- end
76
- else
77
- relation.to_sql
78
- end
79
-
80
- initialize_without_rails(model, cursor_name, query, join_dependency)
81
- end
82
- alias_method_chain :initialize, :rails
83
- end
84
47
  end
85
48
 
86
49
  class ActiveRecord::Relation
@@ -89,10 +52,6 @@ end
89
52
 
90
53
  class ActiveRecord::Base
91
54
  class << self
92
- if ActiveRecord::VERSION::MAJOR >= 4
93
- delegate :cursor, :to => :all
94
- else
95
- delegate :cursor, :to => :scoped
96
- end
55
+ delegate :cursor, to: :all
97
56
  end
98
57
  end
@@ -1,7 +1,7 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  module PostgreSQLCursors
4
- VERSION = '1.0.0'.freeze
5
+ VERSION = '3.0.0'
5
6
  end
6
7
  end
7
-
@@ -1,40 +1,28 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActiveRecord
3
4
  # Exception raised when database cursors aren't supported, which they
4
5
  # absolutely should be in our app.
5
6
  class CursorsNotSupported < ActiveRecordError; end
6
7
 
7
- module PostgreSQLCursors
8
- module JoinDependency
9
- # Extra method we can use to clear out a couple of things in
10
- # JoinDependency so we can use some of the methods for our
11
- # cursors code.
12
- def clear_with_cursor
13
- @reflections = []
14
- @base_records_hash = {}
15
- @base_records_in_order = []
16
- end
17
- end
18
- end
19
-
20
8
  # PostgreSQLCursor is an Enumerable class so you can use each, map,
21
9
  # any? and all of those nice Enumerable methods.
22
10
  #
23
11
  # At the moment, cursors aren't scrollable and are fetch forward-only
24
12
  # and read-only.
25
- #
26
- # This class isn't really meant to be used outside of the
27
- # ActiveRecord::Base#find method.
28
13
  class PostgreSQLCursor
29
14
  include Enumerable
30
15
 
31
- def initialize(model, cursor_name, query, join_dependency = nil)
16
+ def initialize(model, cursor_name, relation, join_dependency = nil)
32
17
  @model = model
33
- @cursor_name = if cursor_name
34
- @model.connection.quote_table_name(cursor_name.gsub(/"/, '\"'))
35
- end
36
- @query = query
18
+ @relation = relation
37
19
  @join_dependency = join_dependency
20
+
21
+ @cursor_name = (@model.connection.quote_table_name(cursor_name.gsub('"', '\"')) if cursor_name)
22
+
23
+ @query = model.connection.unprepared_statement do
24
+ relation.to_sql
25
+ end
38
26
  end
39
27
 
40
28
  def inspect
@@ -45,71 +33,67 @@ module ActiveRecord
45
33
  # record as a parameter.
46
34
  def each
47
35
  @model.transaction do
48
- begin
49
- declare_cursor
50
- if @join_dependency
51
- rows = Array.new
52
- last_id = nil
53
- while row = fetch_forward
54
- current_id = row[@join_dependency.join_base.aliased_primary_key]
55
- last_id ||= current_id
56
- if last_id == current_id
57
- rows << row
58
- last_id = current_id
59
- else
60
- yield @join_dependency.instantiate(rows).first
61
- @join_dependency.clear_with_cursor
62
- rows = [ row ]
63
- end
36
+ declare_cursor
37
+
38
+ if @join_dependency
39
+ rows = []
40
+ last_id = nil
41
+
42
+ until (row = fetch_forward).empty?
43
+ instantiated_row = @join_dependency.instantiate(row, true).first
44
+ current_id = instantiated_row[@join_dependency.send(:join_root).primary_key]
45
+ last_id ||= current_id
46
+
47
+ if last_id == current_id
48
+ rows << row.first.values
64
49
  last_id = current_id
65
- end
50
+ else
51
+ result_set = ActiveRecord::Result.new(row.columns, rows, row.column_types)
66
52
 
67
- if !rows.empty?
68
- yield @join_dependency.instantiate(rows).first
69
- end
70
- else
71
- while row = fetch_forward
72
- yield row
53
+ yield @join_dependency.instantiate(result_set, true).first
54
+
55
+ rows = [row.first.values]
73
56
  end
57
+
58
+ last_id = current_id
59
+ end
60
+
61
+ unless rows.empty?
62
+ result_set = ActiveRecord::Result.new(row.columns, rows, row.column_types)
63
+
64
+ yield @join_dependency.instantiate(result_set, true).first
65
+ end
66
+ else
67
+ until (row = fetch_forward).empty?
68
+ yield @model.instantiate(row.first)
74
69
  end
75
- ensure
76
- close_cursor
77
70
  end
71
+ ensure
72
+ close_cursor
78
73
  end
79
74
  nil
80
75
  end
81
76
 
82
77
  private
78
+
83
79
  def cursor_name
84
- @cursor_name ||= "cursor_#{(rand * 1000000).ceil}"
80
+ @cursor_name ||= "cursor_#{(rand * 1_000_000).ceil}"
85
81
  end
86
82
 
87
- def fetch_forward #:nodoc:
88
- @model.find_by_sql(%{FETCH FORWARD FROM #{cursor_name}}).first
83
+ def fetch_forward # :nodoc:
84
+ @relation.uncached do
85
+ @relation.connection.select_all(%{FETCH FORWARD FROM #{cursor_name}})
86
+ end
89
87
  end
90
88
 
91
- def declare_cursor #:nodoc:
89
+ def declare_cursor # :nodoc:
92
90
  @model.connection.execute(%{DECLARE #{cursor_name} CURSOR FOR #{@query}})
93
91
  end
94
92
 
95
- def close_cursor #:nodoc:
93
+ def close_cursor # :nodoc:
96
94
  @model.connection.execute(%{CLOSE #{cursor_name}})
97
95
  end
98
96
  end
99
97
  end
100
98
 
101
- if ActiveRecord::VERSION::STRING >= '3.1'
102
- class ActiveRecord::Associations::JoinDependency
103
- include ActiveRecord::PostgreSQLCursors::JoinDependency
104
- end
105
- else
106
- class ActiveRecord::Associations::ClassMethods::JoinDependency
107
- include ActiveRecord::PostgreSQLCursors::JoinDependency
108
- end
109
- end
110
-
111
- if ActiveRecord::VERSION::MAJOR >= 3
112
- require File.join(File.dirname(__FILE__), *%w{ active_record postgresql_cursors cursors })
113
- else
114
- require File.join(File.dirname(__FILE__), *%w{ active_record postgresql_cursors cursors_2 })
115
- end
99
+ require File.join(File.dirname(__FILE__), *%w{ active_record postgresql_cursors cursors })
@@ -0,0 +1,17 @@
1
+ sonar.projectKey=dark-panda_activerecord-postgresql-cursors
2
+ sonar.organization=dark-panda
3
+
4
+ # This is the name and version displayed in the SonarCloud UI.
5
+ sonar.projectName=activerecord-postgresql-cursors
6
+ sonar.projectVersion=1.0
7
+
8
+ # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
9
+ sonar.sources=lib, test
10
+ sonar.coverage.exclusions=test
11
+
12
+ # Encoding of the source code. Default is default system encoding
13
+ sonar.sourceEncoding=UTF-8
14
+
15
+ # Additional reports
16
+ sonar.ruby.rubocop.reportPaths=rubocop-report.json
17
+ sonar.ruby.coverage.reportPaths=coverage/coverage.json
@@ -0,0 +1,10 @@
1
+
2
+ arunit:
3
+ encoding: unicode
4
+ min_messages: WARNING
5
+ database: postgresql_cursors_unit_tests
6
+ adapter: postgresql
7
+ user: appuser
8
+ password: password
9
+ host: localhost
10
+ port: 5432
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class PostgreSQLCursorTests < Minitest::Test
6
+ include PostgreSQLCursorTestHelper
7
+
8
+ def test_cursor_scoped
9
+ cursor = Foo.cursor(order: 'id')
10
+
11
+ assert_kind_of(ActiveRecord::PostgreSQLCursor, cursor)
12
+
13
+ assert_equal(%w{ one two three four five }, cursor.collect(&:name))
14
+ end
15
+
16
+ def test_cursor_while_updating
17
+ cursor = Foo.cursor(order: 'id')
18
+
19
+ cursor.each do |row|
20
+ row.name = "#{row.name}_updated"
21
+
22
+ assert(row.save)
23
+ end
24
+
25
+ assert_equal(%w{ one_updated two_updated three_updated four_updated five_updated }, cursor.collect(&:name))
26
+ end
27
+
28
+ def test_with_associations
29
+ cursor = Foo.cursor(order: 'id')
30
+
31
+ cursor.each do |row|
32
+ assert_kind_of(Foo, row)
33
+ row.bars.each do |bar|
34
+ assert_kind_of(Bar, bar)
35
+ end
36
+ end
37
+ end
38
+
39
+ def test_with_associations_eager_loading
40
+ cursor = Foo.cursor(order: 'foos.id', include: :bars)
41
+
42
+ cursor.each do |row|
43
+ assert_kind_of(Foo, row)
44
+ row.bars.each do |bar|
45
+ assert_kind_of(Bar, bar)
46
+ end
47
+ end
48
+ end
49
+
50
+ def test_nested_cursors
51
+ cursor = Foo.cursor(order: 'foos.id')
52
+
53
+ cursor.each do |row|
54
+ bars_cursor = row.bars.cursor
55
+
56
+ assert_kind_of(ActiveRecord::PostgreSQLCursor, bars_cursor)
57
+
58
+ bars_cursor.each do |bar|
59
+ assert_kind_of(Bar, bar)
60
+ end
61
+ end
62
+ end
63
+
64
+ def test_as_relation
65
+ cursor = Foo.order('foos.id').where('foos.id >= 3').cursor
66
+
67
+ assert_equal(3, cursor.to_a.length)
68
+
69
+ cursor.each do |row|
70
+ assert_kind_of(Foo, row)
71
+ assert_equal(2, row.bars.length)
72
+ row.bars.each do |bar|
73
+ assert_kind_of(Bar, bar)
74
+ end
75
+ end
76
+ end
77
+
78
+ def test_as_relation_with_associations
79
+ cursor = Foo.includes(:bars).order('foos.id').where('foos.id >= 3').cursor
80
+
81
+ assert_equal(3, cursor.to_a.length)
82
+
83
+ cursor.each do |row|
84
+ assert_kind_of(Foo, row)
85
+ assert_equal(2, row.bars.length)
86
+ row.bars.each do |bar|
87
+ assert_kind_of(Bar, bar)
88
+ end
89
+ end
90
+ end
91
+ end