light_record 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|