groonga-query-log 1.3.0 → 1.3.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 +4 -4
- data/doc/text/news.md +31 -0
- data/lib/groonga-query-log/command/check-crash.rb +7 -1
- data/lib/groonga-query-log/command/run-regression-test.rb +366 -341
- data/lib/groonga-query-log/command/verify-server.rb +120 -104
- data/lib/groonga-query-log/response-comparer.rb +220 -153
- data/lib/groonga-query-log/server-verifier.rb +189 -175
- data/lib/groonga-query-log/version.rb +1 -1
- data/test/test-response-comparer.rb +136 -4
- metadata +2 -2
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2013-
|
1
|
+
# Copyright (C) 2013-2018 Kouhei Sutou <kou@clear-code.com>
|
2
2
|
#
|
3
3
|
# This library is free software; you can redistribute it and/or
|
4
4
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -19,133 +19,149 @@ require "optparse"
|
|
19
19
|
require "groonga-query-log"
|
20
20
|
|
21
21
|
module GroongaQueryLog
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
22
|
+
module Command
|
23
|
+
class VerifyServer
|
24
|
+
def initialize
|
25
|
+
@options = ServerVerifier::Options.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def run(command_line, &callback)
|
29
|
+
input_paths = create_parser.parse(command_line)
|
30
|
+
same = true
|
31
|
+
verifier = ServerVerifier.new(@options)
|
32
|
+
if input_paths.empty?
|
33
|
+
same = verifier.verify($stdin, &callback)
|
34
|
+
else
|
35
|
+
input_paths.each do |input_path|
|
36
|
+
File.open(input_path) do |input|
|
37
|
+
unless verifier.verify(input, &callback)
|
38
|
+
same = false
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
40
|
-
true
|
41
42
|
end
|
43
|
+
same
|
44
|
+
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
private
|
47
|
+
def create_parser
|
48
|
+
parser = OptionParser.new
|
49
|
+
parser.version = VERSION
|
50
|
+
parser.banner += " QUERY_LOG1 QUERY_LOG2 ..."
|
48
51
|
|
49
|
-
|
50
|
-
|
52
|
+
parser.separator("")
|
53
|
+
parser.separator("Options:")
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
available_protocols = [:gqtp, :http]
|
56
|
+
available_protocols_label = "[#{available_protocols.join(', ')}]"
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
parser.on("--groonga1-host=HOST",
|
59
|
+
"Host name or IP address of Groonga server 1",
|
60
|
+
"[#{@options.groonga1.host}]") do |host|
|
61
|
+
@options.groonga1.host = host
|
62
|
+
end
|
60
63
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
parser.on("--groonga1-port=PORT", Integer,
|
65
|
+
"Port number of Groonga server 1",
|
66
|
+
"[#{@options.groonga1.port}]") do |port|
|
67
|
+
@options.groonga1.port = port
|
68
|
+
end
|
66
69
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
70
|
+
parser.on("--groonga1-protocol=PROTOCOL", available_protocols,
|
71
|
+
"Protocol of Groonga server 1",
|
72
|
+
available_protocols_label) do |protocol|
|
73
|
+
@options.groonga1.protocol = protocol
|
74
|
+
end
|
72
75
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
parser.on("--groonga2-host=HOST",
|
77
|
+
"Host name or IP address of Groonga server 2",
|
78
|
+
"[#{@options.groonga2.host}]") do |host|
|
79
|
+
@options.groonga2.host = host
|
80
|
+
end
|
78
81
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
82
|
+
parser.on("--groonga2-port=PORT", Integer,
|
83
|
+
"Port number of Groonga server 2",
|
84
|
+
"[#{@options.groonga2.port}]") do |port|
|
85
|
+
@options.groonga2.port = port
|
86
|
+
end
|
84
87
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
88
|
+
parser.on("--groonga2-protocol=PROTOCOL", available_protocols,
|
89
|
+
"Protocol of Groonga server 2",
|
90
|
+
available_protocols_label) do |protocol|
|
91
|
+
@options.groonga2.protocol = protocol
|
92
|
+
end
|
90
93
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
94
|
+
parser.on("--n-clients=N", Integer,
|
95
|
+
"The max number of concurrency",
|
96
|
+
"[#{@options.n_clients}]") do |n_clients|
|
97
|
+
@options.n_clients = n_clients
|
98
|
+
end
|
96
99
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
parser.on("--request-queue-size=SIZE", Integer,
|
101
|
+
"The size of request queue",
|
102
|
+
"[auto]") do |size|
|
103
|
+
@options.request_queue_size = size
|
104
|
+
end
|
102
105
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
parser.on("--disable-cache",
|
107
|
+
"Add 'cache=no' parameter to request",
|
108
|
+
"[#{@options.disable_cache?}]") do
|
109
|
+
@options.disable_cache = true
|
110
|
+
end
|
108
111
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
112
|
+
parser.on("--target-command-name=NAME",
|
113
|
+
"Add NAME to target command names",
|
114
|
+
"You can specify this option zero or more times",
|
115
|
+
"See also --target-command-names") do |name|
|
116
|
+
@options.target_command_names << name
|
117
|
+
end
|
115
118
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
119
|
+
target_command_names_label = @options.target_command_names.join(", ")
|
120
|
+
parser.on("--target-command-names=NAME1,NAME2,...", Array,
|
121
|
+
"Replay only NAME1,NAME2,... commands",
|
122
|
+
"You can use glob to choose command name",
|
123
|
+
"[#{target_command_names_label}]") do |names|
|
124
|
+
@options.target_command_names = names
|
125
|
+
end
|
123
126
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
127
|
+
parser.on("--no-care-order",
|
128
|
+
"Don't care order of select response records") do
|
129
|
+
@options.care_order = false
|
130
|
+
end
|
128
131
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
132
|
+
parser.on("--output=PATH",
|
133
|
+
"Output results to PATH",
|
134
|
+
"[stdout]") do |path|
|
135
|
+
@options.output_path = path
|
136
|
+
end
|
134
137
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
138
|
+
parser.on("--[no-]verify-cache",
|
139
|
+
"Verify cache for each query.",
|
140
|
+
"[#{@options.verify_cache?}]") do |verify_cache|
|
141
|
+
@options.verify_cache = verify_cache
|
142
|
+
end
|
140
143
|
|
141
|
-
|
142
|
-
|
144
|
+
parser.on("--ignore-drilldown-key=KEY",
|
145
|
+
"Don't compare drilldown result for KEY",
|
146
|
+
"You can specify multiple drilldown keys by",
|
147
|
+
"specifying this option multiple times") do |key|
|
148
|
+
@options.ignored_drilldown_keys << key
|
149
|
+
end
|
143
150
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
151
|
+
parser.on("--[no-]stop-on-failure",
|
152
|
+
"Stop execution on the first failure",
|
153
|
+
"(#{@options.stop_on_failure?})") do |boolean|
|
154
|
+
@options.stop_on_failure = boolean
|
155
|
+
end
|
156
|
+
|
157
|
+
parser.separator("Debug options:")
|
158
|
+
parser.separator("")
|
159
|
+
|
160
|
+
parser.on("--abort-on-exception",
|
161
|
+
"Abort on exception in threads") do
|
162
|
+
Thread.abort_on_exception = true
|
148
163
|
end
|
149
164
|
end
|
150
165
|
end
|
166
|
+
end
|
151
167
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2014-
|
1
|
+
# Copyright (C) 2014-2018 Kouhei Sutou <kou@clear-code.com>
|
2
2
|
#
|
3
3
|
# This library is free software; you can redistribute it and/or
|
4
4
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -15,199 +15,266 @@
|
|
15
15
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
16
|
|
17
17
|
module GroongaQueryLog
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
class ResponseComparer
|
19
|
+
def initialize(command, response1, response2, options={})
|
20
|
+
@command = command
|
21
|
+
@response1 = response1
|
22
|
+
@response2 = response2
|
23
|
+
@options = options.dup
|
24
|
+
@options[:care_order] = true if @options[:care_order].nil?
|
25
|
+
@options[:ignored_drilldown_keys] ||= []
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else
|
32
|
-
false
|
33
|
-
end
|
28
|
+
def same?
|
29
|
+
if error_response?(@response1) or error_response?(@response2)
|
30
|
+
if error_response?(@response1) and error_response?(@response2)
|
31
|
+
same_error_response?
|
34
32
|
else
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
33
|
+
false
|
34
|
+
end
|
35
|
+
else
|
36
|
+
case @command.name
|
37
|
+
when "select", "logical_select"
|
38
|
+
same_select_response?
|
39
|
+
when "status"
|
40
|
+
same_cache_hit_rate?
|
41
|
+
else
|
42
|
+
same_response?
|
43
43
|
end
|
44
44
|
end
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
private
|
48
|
+
def error_response?(response)
|
49
|
+
response.is_a?(Groonga::Client::Response::Error)
|
50
|
+
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
def same_error_response?
|
53
|
+
return_code1 = @response1.header[0]
|
54
|
+
return_code2 = @response2.header[0]
|
55
|
+
return_code1 == return_code2
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
def same_response?
|
59
|
+
@response1.body == @response2.body
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
else
|
68
|
-
same_response?
|
69
|
-
end
|
62
|
+
def same_select_response?
|
63
|
+
if care_order?
|
64
|
+
if all_output_columns?
|
65
|
+
return false unless same_records_all_output_columns?
|
66
|
+
elsif have_unary_minus_output_column?
|
67
|
+
return false unless same_records_unary_minus_output_column?
|
70
68
|
else
|
71
|
-
|
69
|
+
return false unless same_records?
|
72
70
|
end
|
71
|
+
same_drilldowns?
|
72
|
+
else
|
73
|
+
same_size_response?
|
73
74
|
end
|
75
|
+
end
|
74
76
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
def same_cache_hit_rate?
|
78
|
+
cache_hit_rate1 = @response1.body["cache_hit_rate"]
|
79
|
+
cache_hit_rate2 = @response2.body["cache_hit_rate"]
|
80
|
+
(cache_hit_rate1 - cache_hit_rate2).abs < (10 ** -13)
|
81
|
+
end
|
82
|
+
|
83
|
+
def care_order?
|
84
|
+
return false unless @options[:care_order]
|
85
|
+
return false if random_sort?
|
86
|
+
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def random_score?
|
91
|
+
return false unless @command.respond_to?(:scorer)
|
92
|
+
/\A_score\s*=\s*rand\(\)\z/ === @command.scorer
|
93
|
+
end
|
80
94
|
|
81
|
-
|
82
|
-
|
83
|
-
|
95
|
+
def random_sort?
|
96
|
+
random_score? and score_sort?
|
97
|
+
end
|
84
98
|
|
85
|
-
|
99
|
+
def score_sort?
|
100
|
+
sort_items = @command.sort_keys
|
101
|
+
normalized_sort_items = sort_items.collect do |item|
|
102
|
+
item.gsub(/\A[+-]/, "")
|
86
103
|
end
|
104
|
+
normalized_sort_items.include?("_score")
|
105
|
+
end
|
106
|
+
|
107
|
+
def same_size_response?
|
108
|
+
records_result1 = @response1.body[0] || []
|
109
|
+
records_result2 = @response2.body[0] || []
|
110
|
+
return false if records_result1.size != records_result2.size
|
87
111
|
|
88
|
-
|
89
|
-
|
90
|
-
|
112
|
+
n_hits1 = records_result1[0]
|
113
|
+
n_hits2 = records_result2[0]
|
114
|
+
return false if n_hits1 != n_hits2
|
115
|
+
|
116
|
+
columns1 = records_result1[1]
|
117
|
+
columns2 = records_result2[1]
|
118
|
+
if all_output_columns?
|
119
|
+
columns1.sort_by(&:first) == columns2.sort_by(&:first)
|
120
|
+
else
|
121
|
+
columns1 == columns2
|
91
122
|
end
|
123
|
+
end
|
92
124
|
|
93
|
-
|
94
|
-
|
125
|
+
def have_unary_minus_output_column?
|
126
|
+
output_columns = @command.output_columns
|
127
|
+
return false if output_columns.nil?
|
128
|
+
output_columns.split(/\s*,?\s*/).any? {|column| column.start_with?("-")}
|
129
|
+
end
|
130
|
+
|
131
|
+
def same_records_unary_minus_output_column?
|
132
|
+
records_result1 = @response1.body[0] || []
|
133
|
+
records_result2 = @response2.body[0] || []
|
134
|
+
return false if records_result1.size != records_result2.size
|
135
|
+
|
136
|
+
n_hits1 = records_result1[0]
|
137
|
+
n_hits2 = records_result2[0]
|
138
|
+
return false if n_hits1 != n_hits2
|
139
|
+
|
140
|
+
columns1 = records_result1[1]
|
141
|
+
columns2 = records_result2[1]
|
142
|
+
records1 = records_result1[2..-1]
|
143
|
+
records2 = records_result2[2..-1]
|
144
|
+
|
145
|
+
if columns1.size != columns2.size
|
146
|
+
if columns2.size > columns1.size
|
147
|
+
columns1, columns2 = columns2, columns1
|
148
|
+
records1, records2 = records2, records1
|
149
|
+
end
|
95
150
|
end
|
96
151
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
152
|
+
records1.each_with_index do |record1, record_index|
|
153
|
+
record2 = records2[record_index]
|
154
|
+
column_offset2 = 0
|
155
|
+
columns1.each_with_index do |name, column_index1|
|
156
|
+
column_index2 = column_offset2 + column_index1
|
157
|
+
if name != columns2[column_index2]
|
158
|
+
column_offset2 -= 1
|
159
|
+
next
|
160
|
+
end
|
161
|
+
value1 = record1[column_index1]
|
162
|
+
value1 = normalize_value(value1, columns1[column_index1])
|
163
|
+
value2 = record2[column_index2]
|
164
|
+
value2 = normalize_value(value2, columns2[column_index2])
|
165
|
+
return false if value1 != value2
|
101
166
|
end
|
102
|
-
normalized_sort_items.include?("_score")
|
103
167
|
end
|
104
168
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
169
|
+
true
|
170
|
+
end
|
171
|
+
|
172
|
+
def all_output_columns?
|
173
|
+
output_columns = @command.output_columns
|
174
|
+
output_columns.nil? or
|
175
|
+
/\A\s*\z/ === output_columns or
|
176
|
+
output_columns.split(/\s*,?\s*/).include?("*")
|
177
|
+
end
|
109
178
|
|
110
|
-
|
111
|
-
|
112
|
-
|
179
|
+
def same_records_all_output_columns?
|
180
|
+
records_result1 = @response1.body[0] || []
|
181
|
+
records_result2 = @response2.body[0] || []
|
182
|
+
return false if records_result1.size != records_result2.size
|
113
183
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
184
|
+
n_hits1 = records_result1[0]
|
185
|
+
n_hits2 = records_result2[0]
|
186
|
+
return false if n_hits1 != n_hits2
|
187
|
+
|
188
|
+
columns1 = records_result1[1]
|
189
|
+
columns2 = records_result2[1]
|
190
|
+
return false if columns1.sort_by(&:first) != columns2.sort_by(&:first)
|
191
|
+
|
192
|
+
column_to_index1 = make_column_to_index_map(columns1)
|
193
|
+
column_to_index2 = make_column_to_index_map(columns2)
|
194
|
+
|
195
|
+
records1 = records_result1[2..-1]
|
196
|
+
records2 = records_result2[2..-1]
|
197
|
+
records1.each_with_index do |record1, record_index|
|
198
|
+
record2 = records2[record_index]
|
199
|
+
column_to_index1.each do |name, column_index1|
|
200
|
+
value1 = record1[column_index1]
|
201
|
+
value1 = normalize_value(value1, columns1[column_index1])
|
202
|
+
column_index2 = column_to_index2[name]
|
203
|
+
value2 = record2[column_index2]
|
204
|
+
value2 = normalize_value(value2, columns2[column_index2])
|
205
|
+
return false if value1 != value2
|
120
206
|
end
|
121
207
|
end
|
122
208
|
|
123
|
-
|
124
|
-
|
125
|
-
return false if output_columns.nil?
|
126
|
-
output_columns.split(/\s*,?\s*/).any? {|column| column.start_with?("-")}
|
127
|
-
end
|
209
|
+
true
|
210
|
+
end
|
128
211
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
212
|
+
def same_records?
|
213
|
+
record_set1 = @response1.body[0] || []
|
214
|
+
record_set2 = @response2.body[0] || []
|
215
|
+
same_record_set?(record_set1,
|
216
|
+
record_set2)
|
217
|
+
end
|
133
218
|
|
134
|
-
|
135
|
-
|
136
|
-
return false if n_hits1 != n_hits2
|
219
|
+
def same_record_set?(record_set1, record_set2)
|
220
|
+
return false if record_set1.size != record_set2.size
|
137
221
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
records2 = records_result2[2..-1]
|
222
|
+
n_hits1 = record_set1[0]
|
223
|
+
n_hits2 = record_set2[0]
|
224
|
+
return false if n_hits1 != n_hits2
|
142
225
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
records1, records2 = records2, records1
|
147
|
-
end
|
148
|
-
end
|
226
|
+
columns1 = record_set1[1]
|
227
|
+
columns2 = record_set2[1]
|
228
|
+
return false if columns1 != columns2
|
149
229
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
value2 = record2[column_index2]
|
161
|
-
return false if value1 != value2
|
162
|
-
end
|
230
|
+
records1 = record_set1[2..-1]
|
231
|
+
records2 = record_set2[2..-1]
|
232
|
+
records1.each_with_index do |record1, record_index|
|
233
|
+
record2 = records2[record_index]
|
234
|
+
columns1.each_with_index do |column1, column_index|
|
235
|
+
value1 = record1[column_index]
|
236
|
+
value1 = normalize_value(value1, column1)
|
237
|
+
value2 = record2[column_index]
|
238
|
+
value2 = normalize_value(value2, column1)
|
239
|
+
return false if value1 != value2
|
163
240
|
end
|
164
|
-
|
165
|
-
true
|
166
241
|
end
|
167
242
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
def make_column_to_index_map(columns)
|
247
|
+
map = {}
|
248
|
+
columns.each_with_index do |(name, _), i|
|
249
|
+
map[name] = i
|
173
250
|
end
|
251
|
+
map
|
252
|
+
end
|
174
253
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
n_hits1 = records_result1[0]
|
181
|
-
n_hits2 = records_result2[0]
|
182
|
-
return false if n_hits1 != n_hits2
|
183
|
-
|
184
|
-
columns1 = records_result1[1]
|
185
|
-
columns2 = records_result2[1]
|
186
|
-
return false if columns1.sort_by(&:first) != columns2.sort_by(&:first)
|
187
|
-
|
188
|
-
column_to_index1 = make_column_to_index_map(columns1)
|
189
|
-
column_to_index2 = make_column_to_index_map(columns2)
|
190
|
-
|
191
|
-
records1 = records_result1[2..-1]
|
192
|
-
records2 = records_result2[2..-1]
|
193
|
-
records1.each_with_index do |record1, record_index|
|
194
|
-
record2 = records2[record_index]
|
195
|
-
column_to_index1.each do |name, column_index1|
|
196
|
-
value1 = record1[column_index1]
|
197
|
-
value2 = record2[column_to_index2[name]]
|
198
|
-
return false if value1 != value2
|
199
|
-
end
|
200
|
-
end
|
254
|
+
def same_drilldowns?
|
255
|
+
drilldowns1 = @response1.body[1..-1] || []
|
256
|
+
drilldowns2 = @response2.body[1..-1] || []
|
257
|
+
return false if drilldowns1.size != drilldowns2.size
|
201
258
|
|
202
|
-
|
259
|
+
drilldown_keys = @command.drilldowns
|
260
|
+
ignored_drilldown_keys = @options[:ignored_drilldown_keys]
|
261
|
+
drilldowns1.each_with_index do |drilldown1, drilldown_index|
|
262
|
+
drilldown_key = drilldown_keys[drilldown_index]
|
263
|
+
next if ignored_drilldown_keys.include?(drilldown_key)
|
264
|
+
drilldown2 = drilldowns2[drilldown_index]
|
265
|
+
return false unless same_record_set?(drilldown1, drilldown2)
|
203
266
|
end
|
267
|
+
true
|
268
|
+
end
|
204
269
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
270
|
+
def normalize_value(value, column)
|
271
|
+
type = column[1]
|
272
|
+
case type
|
273
|
+
when "Float"
|
274
|
+
value.round(10)
|
275
|
+
else
|
276
|
+
value
|
211
277
|
end
|
212
278
|
end
|
279
|
+
end
|
213
280
|
end
|