activerecord-postgresql-cursors 1.0.0 → 3.0.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/main.yml +71 -0
- data/.rubocop-minitest.yml +240 -0
- data/.rubocop.yml +5683 -0
- data/.rubocop_todo.yml +26 -0
- data/FUNDING.yml +2 -0
- data/Gemfile +13 -16
- data/Guardfile +8 -10
- data/MIT-LICENSE +1 -2
- data/{README.rdoc → README.md} +17 -10
- data/Rakefile +7 -6
- data/activerecord-postgresql-cursors.gemspec +15 -15
- data/lib/active_record/postgresql_cursors/cursors.rb +14 -55
- data/lib/active_record/postgresql_cursors/version.rb +2 -2
- data/lib/activerecord-postgresql-cursors.rb +50 -66
- data/sonar-project.properties +17 -0
- data/test/ci/github/database.yml +10 -0
- data/test/cursor_test.rb +91 -0
- data/test/test_helper.rb +34 -45
- metadata +22 -19
- data/lib/active_record/postgresql_cursors/cursors_2.rb +0 -75
- data/test/cursor_tests.rb +0 -97
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
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
gem
|
|
12
|
-
gem
|
|
13
|
-
gem
|
|
14
|
-
gem
|
|
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', :
|
|
3
|
-
watch(%r
|
|
3
|
+
guard 'minitest', test_folders: 'test', test_file_patterns: '*_tests.rb' do
|
|
4
|
+
watch(%r{^test/(.+)_tests\.rb})
|
|
4
5
|
|
|
5
|
-
watch(%r
|
|
6
|
-
|
|
6
|
+
watch(%r{^lib/(.*)([^/]+)\.rb}) do |_m|
|
|
7
|
+
'test/cursor_tests.rb'
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
watch(%r
|
|
10
|
-
|
|
10
|
+
watch(%r{^test/test_helper\.rb}) do
|
|
11
|
+
'test'
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
if File.
|
|
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-
|
|
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
|
-
|
data/{README.rdoc → README.md}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
```ruby
|
|
13
|
+
MyModel.find(:cursor, :conditions => 'some_column = true').each do |r|
|
|
14
|
+
puts r.inspect
|
|
15
|
+
end
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
MyModel.find(:cursor).collect { |r| r.foo / PI }.avg
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require File.expand_path('
|
|
3
|
+
require File.expand_path('lib/active_record/postgresql_cursors/version', __dir__)
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |s|
|
|
6
|
-
s.name =
|
|
6
|
+
s.name = 'activerecord-postgresql-cursors'
|
|
7
7
|
s.version = ActiveRecord::PostgreSQLCursors::VERSION
|
|
8
8
|
|
|
9
|
-
s.required_rubygems_version = Gem::Requirement.new(
|
|
10
|
-
s.
|
|
11
|
-
s.
|
|
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 =
|
|
14
|
-
s.license =
|
|
14
|
+
s.email = 'dark.panda@gmail.com'
|
|
15
|
+
s.license = 'MIT'
|
|
15
16
|
s.extra_rdoc_files = [
|
|
16
|
-
|
|
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.
|
|
21
|
-
s.
|
|
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(
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
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 =
|
|
54
|
-
|
|
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
|
-
|
|
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,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,
|
|
16
|
+
def initialize(model, cursor_name, relation, join_dependency = nil)
|
|
32
17
|
@model = model
|
|
33
|
-
@
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
50
|
+
else
|
|
51
|
+
result_set = ActiveRecord::Result.new(row.columns, rows, row.column_types)
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 *
|
|
80
|
+
@cursor_name ||= "cursor_#{(rand * 1_000_000).ceil}"
|
|
85
81
|
end
|
|
86
82
|
|
|
87
|
-
def fetch_forward
|
|
88
|
-
@
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
data/test/cursor_test.rb
ADDED
|
@@ -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
|