light_record 0.2
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 +7 -0
- data/lib/light_record.rb +193 -0
- data/spec/light_record_spec.rb +117 -0
- data/spec/prepare_db.rb +132 -0
- data/spec/test_helper.rb +17 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 68b18b821e94f95075db330967d3cb14537ef4b3
|
4
|
+
data.tar.gz: 6463993d97d10f007faee6c14fda80c2efad8c1e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fc8ebbe93aa163cfc93d1368f3cbfa6c90fa0893d9476336f908aea7f135c76e105515555b19226f2df6c856c3be86047625ad6a0039ebd4baa107d077fccbd4
|
7
|
+
data.tar.gz: 4735d36300f2747a7233e728d05dcd143cd6103b361ded30293351c6754015744c7894609b68147c54d00b94b669fbe710501bcc9da6ceb1aeac948f2e41f78b
|
data/lib/light_record.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
module LightRecord
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Create LightRecord class based on klass argument
|
5
|
+
# Used internally by scope#light_records and scope#light_records_each
|
6
|
+
#
|
7
|
+
# @param klass [Class] ActiveRecord model
|
8
|
+
# @param fields [Array] list of fields, will be used to define attribute methods
|
9
|
+
# @return new class base on klass argument but extended with LightRecord methods
|
10
|
+
#
|
11
|
+
def build_for_class(klass, fields)
|
12
|
+
new_klass = Class.new(klass) do
|
13
|
+
self.table_name = klass.table_name
|
14
|
+
self.inheritance_column = nil
|
15
|
+
|
16
|
+
extend LightRecord::RecordAttributes
|
17
|
+
|
18
|
+
define_fields(fields)
|
19
|
+
|
20
|
+
def initialize(data)
|
21
|
+
@data = data
|
22
|
+
@attributes = @data
|
23
|
+
@readonly = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_attribute_before_type_cast(attr_name)
|
27
|
+
@data[attr_name.to_sym]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.subclass_from_attributes?(attrs)
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_attribute?(attr_name)
|
35
|
+
@attributes.has_key?(attr_name.to_sym)
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_attribute(attr_name)
|
39
|
+
@attributes[attr_name.to_sym]
|
40
|
+
end
|
41
|
+
|
42
|
+
def _read_attribute(attr_name)
|
43
|
+
@attributes.fetch(attr_name.to_sym) { |n| yield n if block_given? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](attr_name)
|
47
|
+
@attributes[attr_name.to_sym]
|
48
|
+
end
|
49
|
+
|
50
|
+
def attributes
|
51
|
+
@attributes
|
52
|
+
end
|
53
|
+
|
54
|
+
# to avoid errors when try saving data
|
55
|
+
def remember_transaction_record_state
|
56
|
+
@_start_transaction_state ||= {}
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if klass.const_defined?(:LightRecord, false)
|
62
|
+
new_klass.send(:include, klass::LightRecord)
|
63
|
+
elsif klass.superclass.const_defined?(:LightRecord, false)
|
64
|
+
new_klass.send(:include, klass.superclass::LightRecord)
|
65
|
+
end
|
66
|
+
|
67
|
+
new_klass
|
68
|
+
end
|
69
|
+
|
70
|
+
# ActiveRecord extension for class methods
|
71
|
+
# Defines klass.define_fields
|
72
|
+
# Overrides klass.column_names and klass.define_attribute_methods
|
73
|
+
module RecordAttributes
|
74
|
+
def define_fields(fields)
|
75
|
+
@fields ||= []
|
76
|
+
|
77
|
+
fields.each do |field|
|
78
|
+
field = field.to_sym unless field.is_a?(Symbol)
|
79
|
+
@fields << field
|
80
|
+
define_method(field) do
|
81
|
+
@data[field]
|
82
|
+
end
|
83
|
+
|
84
|
+
# to avoid errors when try saving data
|
85
|
+
define_method("#{field}=") do |value|
|
86
|
+
@data[field] = value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# ActiveRecord make method :id refers to primary key, even there is no column "id"
|
91
|
+
if !fields.include?(:id) && !fields.include?("id") && primary_key.present?
|
92
|
+
define_method(:id) do
|
93
|
+
@data[self.class.primary_key.to_sym]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# used in Record#respond_to?
|
99
|
+
def define_attribute_methods
|
100
|
+
end
|
101
|
+
|
102
|
+
# Active record keep it as strings, but I keep it as symbols
|
103
|
+
def column_names
|
104
|
+
@fields.map(&:to_s)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create LightRecord class based on klass argument
|
109
|
+
def base_extended(klass)
|
110
|
+
@base_extended ||= {}
|
111
|
+
if @base_extended[klass]
|
112
|
+
return @base_extended[klass]
|
113
|
+
end
|
114
|
+
|
115
|
+
@base_extended[klass] = LightRecord.build_for_class(klass, klass.column_names)
|
116
|
+
end
|
117
|
+
|
118
|
+
module RelationMethods
|
119
|
+
|
120
|
+
# Executes query and return array of light object (model class extended by LightRecord)
|
121
|
+
def light_records(options = {})
|
122
|
+
client = connection.instance_variable_get(:@connection)
|
123
|
+
sql = self.to_sql
|
124
|
+
|
125
|
+
options = {
|
126
|
+
stream: false, symbolize_keys: true, cache_rows: false, as: :hash,
|
127
|
+
database_timezone: ActiveRecord::Base.default_timezone
|
128
|
+
}
|
129
|
+
result = _light_record_execute_query(connection, sql, options)
|
130
|
+
|
131
|
+
klass = LightRecord.build_for_class(self.klass, result.fields)
|
132
|
+
|
133
|
+
if options[:set_const]
|
134
|
+
self.klass.const_set(:"LR_#{Time.now.to_i}", klass)
|
135
|
+
end
|
136
|
+
|
137
|
+
need_symbolize_keys = defined?(PG::Result) && result.is_a?(PG::Result)
|
138
|
+
|
139
|
+
records = []
|
140
|
+
result.each do |row|
|
141
|
+
row.symbolize_keys! if need_symbolize_keys
|
142
|
+
records << klass.new(row)
|
143
|
+
end
|
144
|
+
|
145
|
+
return records
|
146
|
+
end
|
147
|
+
|
148
|
+
# Same as `#light_records` but iterates through result set and call block for each object
|
149
|
+
# this uses less memroy because it creates objects one-by-one
|
150
|
+
# it uses stream feature of mysql client
|
151
|
+
def light_records_each(options = {})
|
152
|
+
conn = ActiveRecord::Base.connection_pool.checkout
|
153
|
+
sql = self.to_sql
|
154
|
+
|
155
|
+
options = {
|
156
|
+
stream: true, symbolize_keys: true, cache_rows: false, as: :hash,
|
157
|
+
database_timezone: ActiveRecord::Base.default_timezone
|
158
|
+
}
|
159
|
+
result = _light_record_execute_query(conn, sql, options)
|
160
|
+
|
161
|
+
klass = LightRecord.build_for_class(self.klass, result.fields)
|
162
|
+
|
163
|
+
if options[:set_const]
|
164
|
+
self.klass.const_set(:"LR_#{Time.now.to_i}", klass)
|
165
|
+
end
|
166
|
+
|
167
|
+
need_symbolize_keys = defined?(PG::Result) && result.is_a?(PG::Result)
|
168
|
+
|
169
|
+
result.each do |row|
|
170
|
+
row.symbolize_keys! if need_symbolize_keys
|
171
|
+
yield klass.new(row)
|
172
|
+
end
|
173
|
+
ensure
|
174
|
+
ActiveRecord::Base.connection_pool.checkin(conn)
|
175
|
+
end
|
176
|
+
|
177
|
+
private def _light_record_execute_query(connection, sql, options)
|
178
|
+
client = connection.instance_variable_get(:@connection)
|
179
|
+
|
180
|
+
if client.class.to_s == 'PG::Connection'
|
181
|
+
connection.execute(sql, "LightRecord - #{self.klass}")
|
182
|
+
else
|
183
|
+
connection.send(:log, sql, "LightRecord - #{self.klass}") do
|
184
|
+
client.query(sql, options)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
ActiveRecord::Relation.send(:include, LightRecord::RelationMethods)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative './test_helper'
|
2
|
+
require_relative './prepare_db'
|
3
|
+
|
4
|
+
#ActiveRecord::Base.logger = Logger.new(STDOUT)
|
5
|
+
|
6
|
+
class ARQuestion < ActiveRecord::Base
|
7
|
+
self.table_name = "sample"
|
8
|
+
self.primary_key = "policy_id"
|
9
|
+
end
|
10
|
+
|
11
|
+
class ARQuestion_wLR < ActiveRecord::Base
|
12
|
+
self.table_name = "sample"
|
13
|
+
self.primary_key = "policy_id"
|
14
|
+
|
15
|
+
module LightRecord
|
16
|
+
def light_included?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "LightRecord" do
|
23
|
+
|
24
|
+
it "should use different connection with #light_records_each" do
|
25
|
+
# Should not raise error
|
26
|
+
iterated = false
|
27
|
+
ARQuestion.all.light_records_each do |sample|
|
28
|
+
iterated = true
|
29
|
+
ARQuestion.first
|
30
|
+
break
|
31
|
+
end
|
32
|
+
|
33
|
+
assert(iterated)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should create class from AR object" do
|
37
|
+
klass = LightRecord.base_extended(ARQuestion)
|
38
|
+
assert(klass < ARQuestion)
|
39
|
+
|
40
|
+
assert_equal(LightRecord.base_extended(ARQuestion).object_id, klass.object_id)
|
41
|
+
|
42
|
+
assert_equal(klass.column_names, ARQuestion.column_names)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should be serializable" do
|
46
|
+
records = ARQuestion.limit(10).light_records
|
47
|
+
assert_equal(records.to_json, ARQuestion.limit(10).to_a.to_json)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should be serializable" do
|
51
|
+
light = ARQuestion.limit(1).light_records.first
|
52
|
+
dark = ARQuestion.find(light.id)
|
53
|
+
|
54
|
+
assert_equal(light.attributes, dark.attributes.symbolize_keys)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should include LightRecord submodule if present" do
|
58
|
+
record = ARQuestion_wLR.limit(1).light_records.first
|
59
|
+
assert(record.light_included?)
|
60
|
+
|
61
|
+
klass = LightRecord.base_extended(ARQuestion_wLR)
|
62
|
+
assert_includes(klass.ancestors, ARQuestion_wLR::LightRecord)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should work with #respond_to?" do
|
66
|
+
rand_sql_fn = ENV['DB'] == 'postgres' ? "random" : "rand"
|
67
|
+
light = ARQuestion.select("*, #{rand_sql_fn}() as extra_column_from_sql").limit(1).light_records.first
|
68
|
+
|
69
|
+
assert_respond_to(light, :extra_column_from_sql)
|
70
|
+
ARQuestion.column_names.each do |column_name|
|
71
|
+
assert_respond_to(light, column_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "sgould have primary key" do
|
76
|
+
light = ARQuestion.limit(1).light_records.first
|
77
|
+
dark = ARQuestion.find(light.id)
|
78
|
+
|
79
|
+
assert_equal(light.id, light[ARQuestion.primary_key])
|
80
|
+
assert_equal(light.id, dark.id)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should be read-only" do
|
84
|
+
record = ARQuestion_wLR.limit(1).light_records.first
|
85
|
+
|
86
|
+
assert(record.readonly?)
|
87
|
+
|
88
|
+
assert_raises(ActiveRecord::ReadOnlyRecord) do
|
89
|
+
record.update_attributes(ARQuestion.column_names.first => "bla bla lba")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should be not as a new record" do
|
94
|
+
record = ARQuestion_wLR.limit(1).light_records.first
|
95
|
+
refute(record.new_record?)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should add extra params in query" do
|
99
|
+
record = ARQuestion_wLR.where(policy_id: 119736).light_records.first
|
100
|
+
assert_equal(record.id, 119736)
|
101
|
+
end
|
102
|
+
|
103
|
+
if ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
104
|
+
it "should select datetime in UTC" do
|
105
|
+
# Active Record will set :database_timezone on first query
|
106
|
+
# But if no query run on that connection then it will be blank
|
107
|
+
# This code is simulating that behaviour
|
108
|
+
ActiveRecord::Base.connection_pool.connections.each do |conn|
|
109
|
+
client = ActiveRecord::Base.connection.instance_variable_get(:@connection)
|
110
|
+
client.query_options.delete(:database_timezone)
|
111
|
+
end
|
112
|
+
|
113
|
+
record = ARQuestion_wLR.select("now() as time").light_records.first
|
114
|
+
assert(record.time.gmt?, "Time is not UTC")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/prepare_db.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require "mysql2"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
if ENV['DB'] == 'postgres'
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
host: '127.0.0.1',
|
7
|
+
adapter: 'postgresql',
|
8
|
+
encoding: 'unicode',
|
9
|
+
database: 'light_record',
|
10
|
+
pool: 5,
|
11
|
+
username: ENV['DB_USER'] || ENV["USER"],
|
12
|
+
password: '',
|
13
|
+
reconnect: true
|
14
|
+
)
|
15
|
+
else
|
16
|
+
ActiveRecord::Base.establish_connection(
|
17
|
+
adapter: 'mysql2',
|
18
|
+
database: 'light_record',
|
19
|
+
host: 'localhost',
|
20
|
+
username: 'root',
|
21
|
+
password: '',
|
22
|
+
pool: 5
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
27
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
28
|
+
|
29
|
+
module TestDB
|
30
|
+
|
31
|
+
SAMPLE_TABLE = 'sample'
|
32
|
+
|
33
|
+
def self.init
|
34
|
+
db = ActiveRecord::Base.connection
|
35
|
+
|
36
|
+
if db.data_source_exists?(SAMPLE_TABLE)
|
37
|
+
puts "Testing table already exists"
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
if db.is_a? ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
42
|
+
db.execute(%{
|
43
|
+
CREATE TABLE `#{SAMPLE_TABLE}` (
|
44
|
+
`policy_id` int(11) NOT NULL,
|
45
|
+
`statecode` varchar(255) DEFAULT NULL,
|
46
|
+
`county` varchar(255) DEFAULT NULL,
|
47
|
+
`eq_site_limit` varchar(255) DEFAULT NULL,
|
48
|
+
`hu_site_limit` varchar(255) DEFAULT NULL,
|
49
|
+
`fl_site_limit` varchar(255) DEFAULT NULL,
|
50
|
+
`fr_site_limit` varchar(255) DEFAULT NULL,
|
51
|
+
`tiv_2011` varchar(255) DEFAULT NULL,
|
52
|
+
`tiv_2012` varchar(255) DEFAULT NULL,
|
53
|
+
`eq_site_deductible` float DEFAULT NULL,
|
54
|
+
`hu_site_deductible` varchar(255) DEFAULT NULL,
|
55
|
+
`fl_site_deductible` int(11) DEFAULT NULL,
|
56
|
+
`fr_site_deductible` int(11) DEFAULT NULL,
|
57
|
+
`point_latitude` varchar(255) DEFAULT NULL,
|
58
|
+
`point_longitude` varchar(255) DEFAULT NULL,
|
59
|
+
`line` varchar(255) DEFAULT NULL,
|
60
|
+
`construction` varchar(255) DEFAULT NULL,
|
61
|
+
`point_granularity` int(11) DEFAULT NULL,
|
62
|
+
PRIMARY KEY (`policy_id`)
|
63
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
64
|
+
})
|
65
|
+
elsif db.is_a? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
66
|
+
db.execute(%{
|
67
|
+
CREATE TABLE #{SAMPLE_TABLE} (
|
68
|
+
policy_id integer NULL,
|
69
|
+
statecode varchar(255) NULL,
|
70
|
+
county varchar(255) NULL,
|
71
|
+
eq_site_limit varchar(255) NULL,
|
72
|
+
hu_site_limit varchar(255) NULL,
|
73
|
+
fl_site_limit varchar(255) NULL,
|
74
|
+
fr_site_limit varchar(255) NULL,
|
75
|
+
tiv_2011 varchar(255) NULL,
|
76
|
+
tiv_2012 varchar(255) NULL,
|
77
|
+
eq_site_deductible real NULL,
|
78
|
+
hu_site_deductible varchar(255) NULL,
|
79
|
+
fl_site_deductible real NULL,
|
80
|
+
fr_site_deductible real NULL,
|
81
|
+
point_latitude varchar(255) NULL,
|
82
|
+
point_longitude varchar(255) NULL,
|
83
|
+
line varchar(255) DEFAULT NULL,
|
84
|
+
construction varchar(255) NULL,
|
85
|
+
point_granularity real NULL,
|
86
|
+
PRIMARY KEY(policy_id)
|
87
|
+
);
|
88
|
+
})
|
89
|
+
end
|
90
|
+
|
91
|
+
import_csv(db)
|
92
|
+
|
93
|
+
ensure
|
94
|
+
#db.execute(%{drop table #{SAMPLE_TABLE}})
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.import_csv(db)
|
98
|
+
require 'csv'
|
99
|
+
|
100
|
+
index = 0
|
101
|
+
headers = nil
|
102
|
+
rows_to_insert = []
|
103
|
+
|
104
|
+
puts "Importing testing data"
|
105
|
+
|
106
|
+
CSV.foreach(File.dirname(__FILE__) + "/../test_data/FL_insurance_sample.csv", headers: true) do |row|
|
107
|
+
headers ||= row.headers
|
108
|
+
|
109
|
+
row_values = []
|
110
|
+
headers.size.times do |i|
|
111
|
+
row_values << %{'#{db.quote_string(row.field(i))}'}
|
112
|
+
end
|
113
|
+
|
114
|
+
rows_to_insert << row_values
|
115
|
+
|
116
|
+
if index % 3000 == 0 && index > 0 || index == 36633
|
117
|
+
puts "Row #{index}"
|
118
|
+
|
119
|
+
values = rows_to_insert.map {|r| '(' + r.join(", ") + ')' }.join(', ')
|
120
|
+
|
121
|
+
db.execute(%{
|
122
|
+
insert into #{SAMPLE_TABLE} (#{headers.join(", ")}) values #{values}
|
123
|
+
})
|
124
|
+
rows_to_insert.clear
|
125
|
+
end
|
126
|
+
|
127
|
+
index += 1
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
TestDB.init
|
data/spec/test_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require "minitest/reporters"
|
3
|
+
|
4
|
+
Minitest::Reporters.use!(
|
5
|
+
Minitest::Reporters::DefaultReporter.new(color: true)
|
6
|
+
)
|
7
|
+
|
8
|
+
def MiniTest.filter_backtrace(bt)
|
9
|
+
bt
|
10
|
+
end
|
11
|
+
|
12
|
+
require "mysql2"
|
13
|
+
require "active_record"
|
14
|
+
require 'light_record'
|
15
|
+
require 'looksee'
|
16
|
+
|
17
|
+
require "minitest/autorun"
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: light_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pavel Evstigneev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-03 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- pavel.evst@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/light_record.rb
|
21
|
+
- spec/light_record_spec.rb
|
22
|
+
- spec/prepare_db.rb
|
23
|
+
- spec/test_helper.rb
|
24
|
+
homepage: http://github.com/paxa/light_record
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.5.1
|
45
|
+
signing_key:
|
46
|
+
specification_version: 3
|
47
|
+
summary: Getting process memory
|
48
|
+
test_files:
|
49
|
+
- spec/light_record_spec.rb
|
50
|
+
- spec/prepare_db.rb
|
51
|
+
- spec/test_helper.rb
|