ar-ondemand 1.0.1

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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Yjk2ZDkxMGM4MDFkY2E4MmVhY2U0ZjVhNDFiZjE5YmMyZmY2ZjkyYw==
5
+ data.tar.gz: !binary |-
6
+ MGYwY2ZlNzZkYjZhMjYxNGVkYTUyZjljMTQwZDVkMDQyYTVhNWNhNg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NDA3ZmVlZDQxNzgyZDUwYjE2MmQwZWNiNjJkYmIyNGJhMjQyMmY0ZGRmMDY5
10
+ MWQ0ZWNmZWVjNTFjNzk0N2M0NjRlNzg0NTFjMzgxMTAyNjUwMzliMzQzZjJl
11
+ ZTRkZjYzNDVjOWI5N2YzZmQ3ZTU4NjRjMWY2NWJmMjIxZjA5MTE=
12
+ data.tar.gz: !binary |-
13
+ MzA3Yjc4YmQ3ODA4NTIxMTFjNzIyY2M4MjhkMGM0MTQ4YTM3YTJlMTgxNWQy
14
+ ZWNhZDFiNjQ0OTA3YzczODE5ZDcwMGRkODhhODU5NDVhMDU3ZGMyODkwODll
15
+ OTkxMTgwMzFmYjY4MTJlYmYzZGJiYWFkOTZmN2Y2NTZkODMwZTM=
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # ar-ondemand
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require File.dirname(__FILE__) + '/lib/ar-ondemand/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'ar-ondemand'
7
+ s.version = ::ArOnDemand::VERSION
8
+ s.date = '2015-01-06'
9
+ s.summary = 'ActiveRecord On-demand'
10
+ s.description = 'Fast access to database results without the memory overhead of ActiveRecord objects'
11
+ s.authors = ['Steve Frank']
12
+ s.email = %w(steve@cloudhealthtech.com lardcanoe@gmail.com)
13
+ s.homepage = 'https://github.com/CloudHealth/ar-ondemand'
14
+ s.license = 'MIT'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+
21
+ s.require_paths = %w(lib)
22
+
23
+ s.add_dependency 'activesupport', '>= 3.2'
24
+ s.add_dependency 'activerecord', '>= 3.2'
25
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/concern'
2
+ require 'ar-ondemand/result'
3
+ require 'ar-ondemand/record'
4
+
5
+ module ActiveRecord
6
+ module OnDemand
7
+ module ForReadingExtension
8
+ extend ::ActiveSupport::Concern
9
+
10
+ # Ripped from the find_in_batches function, but customized to return an ::ActiveRecord::OnDemand::ResultSet
11
+ def for_reading(options = {})
12
+ return query(self) if options.empty?
13
+
14
+ relation = self
15
+
16
+ unless arel.orders.blank? && arel.taken.blank?
17
+ ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
18
+ end
19
+
20
+ if (finder_options = options.except(:start, :batch_size)).present?
21
+ raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present?
22
+ raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present?
23
+
24
+ relation = apply_finder_options(finder_options)
25
+ end
26
+
27
+ start = options.delete(:start)
28
+ batch_size = options.delete(:batch_size) || 1000
29
+
30
+ relation = relation.reorder(batch_order).limit(batch_size)
31
+ records = start ? query(relation.where(table[primary_key].gteq(start))) : query(relation)
32
+
33
+ while records.any?
34
+ records_size = records.size
35
+ primary_key_offset = records.last.id
36
+
37
+ yield records
38
+
39
+ break if records_size < batch_size
40
+
41
+ if primary_key_offset
42
+ records = query relation.where(table[primary_key].gt(primary_key_offset))
43
+ else
44
+ raise "Primary key not included in the custom select clause"
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def query(ar)
52
+ results = ::ActiveRecord::Base.connection.exec_query ar.to_sql
53
+ ::ActiveRecord::OnDemand::ResultSet.new self.arel.engine, results
54
+ end
55
+
56
+ def batch_order
57
+ "#{quoted_table_name}.#{quoted_primary_key} ASC"
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ ::ActiveRecord::Relation.send(:include, ::ActiveRecord::OnDemand::ForReadingExtension)
@@ -0,0 +1,58 @@
1
+ require 'active_support/concern'
2
+ require 'ar-ondemand/result'
3
+ require 'ar-ondemand/record'
4
+
5
+ module ActiveRecord
6
+ module OnDemand
7
+ class Result < ResultSet
8
+ include ::Enumerable
9
+
10
+ def initialize(model, results, key_column, defaults)
11
+ super(model, results)
12
+
13
+ raise "Key column cannot be blank." if key_column.blank?
14
+ raise "Defaults cannot be empty." if defaults.empty?
15
+
16
+ @key_column = key_column.to_s
17
+ @defaults = defaults
18
+
19
+ @key_index = @col_indexes[@key_column]
20
+ raise "Unknown index #{key_column}" if @key_index.nil?
21
+
22
+ @find_by_method = "find_or_initialize_by_#{(@defaults.keys + [@key_column]).join('_and_')}"
23
+ @has_any_assets = !ids.empty? || @model.unscoped.where(@defaults).exists?
24
+ @new_params = @defaults.dup
25
+ @new_params[@key_column.to_sym] = nil
26
+ end
27
+
28
+ def [](key)
29
+ raise "Search key cannot be blank." if key.blank?
30
+ rec = @results.rows.find { |x| x[@key_index] == key }
31
+ if rec.nil?
32
+ if @has_any_assets
33
+ args = @defaults.values + [key]
34
+ @model.unscoped.send(@find_by_method, *args)
35
+ else
36
+ @new_params[@key_column.to_sym] = key
37
+ @model.new @new_params
38
+ end
39
+ else
40
+ ::ActiveRecord::OnDemand::Record.new convert_to_hash(rec), @model, @defaults
41
+ end
42
+ end
43
+ end
44
+
45
+ module Extension
46
+ extend ::ActiveSupport::Concern
47
+
48
+ module ClassMethods
49
+ def on_demand(keys, defaults = {})
50
+ results = ::ActiveRecord::Base.connection.exec_query self.where(defaults).to_sql
51
+ ::ActiveRecord::OnDemand::Result.new self, results, keys, defaults
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ ::ActiveRecord::Base.send(:include, ::ActiveRecord::OnDemand::Extension)
@@ -0,0 +1,69 @@
1
+ module ActiveRecord
2
+ module OnDemand
3
+ class Record
4
+ #TODO: include ::ActiveModel::Dirty
5
+ attr_reader :changes
6
+
7
+ def initialize(record, model, defaults)
8
+ @record = record
9
+ @model = model
10
+ @defaults = defaults
11
+ @changes = {}
12
+ end
13
+
14
+ def is_readonly?
15
+ @defaults.nil?
16
+ end
17
+
18
+ def method_missing(meth, *args, &block)
19
+ key = meth.to_s
20
+ if @record.include? key
21
+ @changes.include?(key) ? @changes[key] : @record[key]
22
+ elsif key.end_with?('=') && @record.include?(key[0...-1])
23
+ raise ActiveRecord::ReadOnlyRecord if is_readonly?
24
+ key = key[0...-1]
25
+ unless key == 'id'
26
+ if @record[key] != args[0]
27
+ return if @record[key] == 1 && args[0] == true
28
+ return if @record[key] == 0 && args[0] == false
29
+ return if args[0].is_a?(Time) && !@record[key].blank? && @record[key].to_time.to_i == args[0].to_i
30
+ @changes[key] = args[0]
31
+ else
32
+ # If they changed it, then reverted back, remove from @changes
33
+ @changes.delete key
34
+ end
35
+ end
36
+ elsif key.end_with?('?') && @record.include?(key[0...-1])
37
+ key = key[0...-1]
38
+ v = @changes.include?(key) ? @changes[key] : @record[key]
39
+ return false if v.nil?
40
+ return false if v === 0 || v === false
41
+ return false if v.respond_to?(:blank?) && v.blank?
42
+ true
43
+ elsif key.end_with?('_changed?') && @record.include?(key[0...-9])
44
+ @changes.include?(key[0...-9])
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def has_attribute?(attr)
51
+ @record.include? attr
52
+ end
53
+
54
+ def save
55
+ raise ActiveRecord::ReadOnlyRecord if is_readonly?
56
+ return nil if @changes.empty?
57
+ # logger.debug "Loading model to store changes: #{@changes}"
58
+ rec = @model.allocate.init_with('attributes' => @record)
59
+ @changes.each_pair do |key, val|
60
+ next if key == 'id'
61
+ rec[key] = val
62
+ end
63
+ rec.save
64
+ @changes.clear
65
+ rec
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,54 @@
1
+ module ActiveRecord
2
+ module OnDemand
3
+ class ResultSet
4
+ include ::Enumerable
5
+
6
+ def initialize(model, results)
7
+ @model = model
8
+ @results = results
9
+ @column_types = Hash[@model.columns.map { |x| [x.name, x] }]
10
+ @col_indexes = HashWithIndifferentAccess[@results.columns.each_with_index.map { |x, i| [x,i] }]
11
+ end
12
+
13
+ def ids
14
+ id_col = @col_indexes[:id]
15
+ @results.rows.map { |r| r[id_col] }
16
+ end
17
+
18
+ def length
19
+ @results.rows.length
20
+ end
21
+ alias_method :count, :length
22
+ alias_method :size, :length
23
+
24
+ def each
25
+ @results.rows.each do |row|
26
+ yield ::ActiveRecord::OnDemand::Record.new(convert_to_hash(row), @model, nil)
27
+ end
28
+ end
29
+
30
+ def first
31
+ row = @results.rows.first
32
+ return nil if row.nil?
33
+ ::ActiveRecord::OnDemand::Record.new convert_to_hash(row), @model, nil
34
+ end
35
+
36
+ def last
37
+ row = @results.rows.last
38
+ return nil if row.nil?
39
+ ::ActiveRecord::OnDemand::Record.new convert_to_hash(row), @model, nil
40
+ end
41
+
42
+ protected
43
+
44
+ def convert_to_hash(rec)
45
+ # TODO: Is using HashWithIndifferentAccess[] more efficient?
46
+ h = {}
47
+ @col_indexes.each_pair do |k, v|
48
+ h[k] = @column_types[k].type_cast rec[v]
49
+ end
50
+ h.with_indifferent_access
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module ArOnDemand
2
+ VERSION = '1.0.1'
3
+ end
@@ -0,0 +1,2 @@
1
+ require_dependency 'ar-ondemand/on_demand'
2
+ require_dependency 'ar-ondemand/for_reading'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ar-ondemand
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Steve Frank
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ description: Fast access to database results without the memory overhead of ActiveRecord
42
+ objects
43
+ email:
44
+ - steve@cloudhealthtech.com
45
+ - lardcanoe@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - README.md
51
+ - ar-ondemand.gemspec
52
+ - lib/ar-ondemand.rb
53
+ - lib/ar-ondemand/for_reading.rb
54
+ - lib/ar-ondemand/on_demand.rb
55
+ - lib/ar-ondemand/record.rb
56
+ - lib/ar-ondemand/result.rb
57
+ - lib/ar-ondemand/version.rb
58
+ homepage: https://github.com/CloudHealth/ar-ondemand
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.3.0
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: ActiveRecord On-demand
82
+ test_files: []
83
+ has_rdoc: