fluent-plugin-droonga 0.9.9 → 1.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.dir-locals.el +3 -0
  3. data/.travis.yml +6 -2
  4. data/README.md +6 -7
  5. data/Rakefile +23 -6
  6. data/fluent-plugin-droonga.gemspec +2 -2
  7. data/lib/droonga/adapter.rb +12 -3
  8. data/lib/droonga/adapter_runner.rb +28 -23
  9. data/lib/droonga/catalog/base.rb +7 -111
  10. data/lib/droonga/catalog/dataset.rb +13 -25
  11. data/lib/droonga/catalog/errors.rb +94 -0
  12. data/lib/droonga/catalog/schema.rb +277 -0
  13. data/lib/droonga/catalog/version1.rb +404 -0
  14. data/lib/droonga/catalog/version2.rb +160 -0
  15. data/lib/droonga/catalog_loader.rb +27 -4
  16. data/lib/droonga/catalog_observer.rb +44 -6
  17. data/lib/droonga/collector.rb +12 -10
  18. data/lib/droonga/{handler_plugin.rb → collector_message.rb} +47 -20
  19. data/lib/droonga/collector_runner.rb +64 -0
  20. data/lib/droonga/collectors.rb +18 -0
  21. data/lib/droonga/{catalog.rb → collectors/add.rb} +9 -7
  22. data/lib/droonga/{command_repository.rb → collectors/and.rb} +7 -14
  23. data/lib/droonga/collectors/sum.rb +26 -0
  24. data/lib/droonga/dispatcher.rb +74 -41
  25. data/lib/droonga/distributed_command_planner.rb +2 -2
  26. data/lib/droonga/engine.rb +13 -5
  27. data/lib/droonga/{message_processing_error.rb → error.rb} +33 -12
  28. data/lib/droonga/{plugin/planner/search.rb → error_messages.rb} +12 -10
  29. data/lib/droonga/farm.rb +15 -14
  30. data/lib/droonga/fluent_message_sender.rb +15 -11
  31. data/lib/droonga/forwarder.rb +22 -18
  32. data/lib/droonga/handler.rb +8 -2
  33. data/lib/droonga/handler_runner.rb +47 -26
  34. data/lib/droonga/input_message.rb +6 -6
  35. data/lib/droonga/{command.rb → loggable.rb} +7 -14
  36. data/lib/droonga/logger.rb +56 -15
  37. data/lib/droonga/message_matcher.rb +12 -7
  38. data/lib/droonga/message_pusher.rb +8 -4
  39. data/lib/droonga/message_receiver.rb +11 -9
  40. data/lib/droonga/output_message.rb +2 -0
  41. data/lib/droonga/planner.rb +21 -10
  42. data/lib/droonga/plugin.rb +15 -0
  43. data/lib/droonga/plugin/metadata/{adapter_message.rb → adapter_input_message.rb} +6 -14
  44. data/lib/droonga/plugin/metadata/adapter_output_message.rb +39 -0
  45. data/lib/droonga/plugin/metadata/collector_message.rb +39 -0
  46. data/lib/droonga/plugin/metadata/input_message.rb +15 -0
  47. data/lib/droonga/plugin_loader.rb +33 -25
  48. data/lib/droonga/plugin_registry.rb +9 -1
  49. data/lib/droonga/plugins/basic.rb +54 -0
  50. data/lib/droonga/plugins/crud.rb +36 -15
  51. data/lib/droonga/plugins/error.rb +5 -4
  52. data/lib/droonga/plugins/groonga.rb +9 -6
  53. data/lib/droonga/plugins/groonga/column_create.rb +10 -5
  54. data/lib/droonga/plugins/groonga/generic_command.rb +2 -8
  55. data/lib/droonga/plugins/groonga/generic_response.rb +2 -2
  56. data/lib/droonga/plugins/groonga/select.rb +2 -2
  57. data/lib/droonga/plugins/groonga/table_create.rb +9 -4
  58. data/lib/droonga/plugins/groonga/table_remove.rb +10 -5
  59. data/lib/droonga/plugins/search.rb +106 -5
  60. data/lib/droonga/plugins/search/distributed_search_planner.rb +398 -0
  61. data/lib/droonga/plugins/watch.rb +41 -20
  62. data/lib/droonga/processor.rb +12 -9
  63. data/lib/droonga/{plugin/collector/basic.rb → reducer.rb} +36 -50
  64. data/lib/droonga/replier.rb +7 -4
  65. data/lib/droonga/searcher.rb +40 -37
  66. data/lib/droonga/server.rb +8 -6
  67. data/lib/droonga/session.rb +17 -7
  68. data/lib/droonga/single_step.rb +53 -0
  69. data/lib/droonga/{plugin/planner/watch.rb → single_step_definition.rb} +27 -26
  70. data/lib/droonga/{partition.rb → slice.rb} +23 -12
  71. data/lib/droonga/status_code.rb +25 -0
  72. data/lib/droonga/step_runner.rb +63 -0
  73. data/lib/droonga/watch_schema.rb +7 -3
  74. data/lib/droonga/watcher.rb +4 -4
  75. data/lib/droonga/worker.rb +6 -6
  76. data/lib/fluent/plugin/out_droonga.rb +27 -2
  77. data/sample/cluster/catalog.json +33 -32
  78. data/test/command/config/default/catalog.json +72 -45
  79. data/test/command/config/version1/catalog.json +68 -0
  80. data/test/command/config/version1/fluentd.conf +11 -0
  81. data/test/command/suite/message/error/missing-dataset.expected +1 -1
  82. data/test/command/suite/message/error/unknown-dataset.expected +1 -1
  83. data/test/command/suite/message/error/unknown-type.expected +13 -0
  84. data/test/command/suite/message/error/{unknown-command.test → unknown-type.test} +1 -1
  85. data/test/command/suite/search/error/missing-source-parameter.expected +1 -1
  86. data/test/command/suite/search/error/unknown-source.expected +15 -3
  87. data/test/command/suite/watch/subscribe.expected +1 -3
  88. data/test/command/suite/watch/unsubscribe.expected +1 -3
  89. data/test/performance/watch/catalog.json +1 -0
  90. data/test/unit/catalog/test_dataset.rb +16 -358
  91. data/test/unit/catalog/test_schema.rb +285 -0
  92. data/test/unit/catalog/test_version1.rb +222 -28
  93. data/test/unit/catalog/test_version2.rb +155 -0
  94. data/test/unit/fixtures/catalog/version2.json +62 -0
  95. data/test/unit/helper/distributed_search_planner_helper.rb +2 -2
  96. data/test/unit/plugins/crud/test_add.rb +13 -13
  97. data/test/unit/plugins/groonga/test_column_create.rb +14 -11
  98. data/test/unit/plugins/groonga/test_table_create.rb +4 -9
  99. data/test/unit/plugins/groonga/test_table_remove.rb +4 -9
  100. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_basic.rb +0 -0
  101. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_group_by.rb +0 -0
  102. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_output.rb +0 -0
  103. data/test/unit/{plugin/planner/search_planner → plugins/search/planner}/test_sort_by.rb +0 -0
  104. data/test/unit/{plugin/collector/test_search.rb → plugins/search/test_collector.rb} +40 -39
  105. data/test/unit/plugins/{test_search.rb → search/test_handler.rb} +6 -5
  106. data/test/unit/{plugin/planner/test_search.rb → plugins/search/test_planner.rb} +3 -3
  107. data/test/unit/{plugin/collector → plugins}/test_basic.rb +68 -50
  108. data/test/unit/plugins/test_groonga.rb +2 -15
  109. data/test/unit/plugins/test_watch.rb +25 -22
  110. data/test/unit/test_message_matcher.rb +29 -6
  111. data/test/unit/test_output.rb +4 -0
  112. metadata +58 -50
  113. data/lib/droonga/collector_plugin.rb +0 -50
  114. data/lib/droonga/legacy_pluggable.rb +0 -66
  115. data/lib/droonga/legacy_plugin.rb +0 -57
  116. data/lib/droonga/legacy_plugin_repository.rb +0 -54
  117. data/lib/droonga/planner_plugin.rb +0 -54
  118. data/lib/droonga/plugin/collector/search.rb +0 -98
  119. data/lib/droonga/plugin/planner/crud.rb +0 -49
  120. data/lib/droonga/plugin/planner/distributed_search_planner.rb +0 -393
  121. data/lib/droonga/plugin/planner/groonga.rb +0 -54
  122. data/lib/droonga/plugin_registerable.rb +0 -75
  123. data/test/command/suite/message/error/unknown-command.expected +0 -13
  124. data/test/unit/test_command_repository.rb +0 -39
  125. data/test/unit/test_legacy_plugin.rb +0 -50
  126. data/test/unit/test_legacy_plugin_repository.rb +0 -89
@@ -1,54 +0,0 @@
1
- # Copyright (C) 2013-2014 Droonga Project
2
- #
3
- # This library is free software; you can redistribute it and/or
4
- # modify it under the terms of the GNU Lesser General Public
5
- # License version 2.1 as published by the Free Software Foundation.
6
- #
7
- # This library is distributed in the hope that it will be useful,
8
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10
- # Lesser General Public License for more details.
11
- #
12
- # You should have received a copy of the GNU Lesser General Public
13
- # License along with this library; if not, write to the Free Software
14
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
-
16
- module Droonga
17
- class LegacyPluginRepository
18
- include Enumerable
19
-
20
- def initialize
21
- @plugins = {}
22
- end
23
-
24
- def each(&block)
25
- @plugins.each(&block)
26
- end
27
-
28
- def register(name, klass)
29
- @plugins[name] = klass
30
- end
31
-
32
- def [](name)
33
- @plugins[name]
34
- end
35
-
36
- def clear
37
- @plugins.clear
38
- end
39
-
40
- def instantiate(name, *args, &block)
41
- plugin_class = self[name]
42
- if plugin_class.nil?
43
- # TODO: use the original error
44
- raise ArgumentError, "unknown plugin: <#{name}>"
45
- end
46
- begin
47
- plugin_class.new(*args, &block)
48
- rescue
49
- p [plugin_class, plugin_class.method(:new), plugin_class.method(:new).arity, args.size]
50
- raise
51
- end
52
- end
53
- end
54
- end
@@ -1,54 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013-2014 Droonga Project
4
- #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
8
- #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
-
18
- require "droonga/legacy_plugin"
19
- require "droonga/distributed_command_planner"
20
-
21
- module Droonga
22
- class PlannerPlugin < LegacyPlugin
23
- extend PluginRegisterable
24
-
25
- def initialize(planner)
26
- super()
27
- @planner = planner
28
- end
29
-
30
- def scatter(message, options={})
31
- planner = DistributedCommandPlanner.new(message)
32
- planner.scatter
33
- planner.key = options[:key]
34
- planner.reduce(options[:reduce])
35
- planner.plan
36
- end
37
-
38
- def broadcast(message, options={})
39
- planner = DistributedCommandPlanner.new(message)
40
- planner.broadcast(:write => options[:write])
41
- planner.reduce(options[:reduce])
42
- planner.plan
43
- end
44
-
45
- private
46
- def process_error(command, error, arguments)
47
- if error.is_a?(MessageProcessingError)
48
- raise error
49
- else
50
- super
51
- end
52
- end
53
- end
54
- end
@@ -1,98 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013-2014 Droonga Project
4
- #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
8
- #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
-
18
- require "droonga/plugin/collector/basic"
19
-
20
- module Droonga
21
- class SearchCollector < BasicCollector
22
- repository.register("search", self)
23
-
24
- command :collector_search_gather
25
- def collector_search_gather(result)
26
- output = body ? body[input_name] : input_name
27
- if output.is_a?(Hash)
28
- elements = output["elements"]
29
- if elements && elements.is_a?(Hash)
30
- # because "count" mapper requires all records,
31
- # I have to apply it at first, before "limit" and "offset" are applied.
32
- count_mapper = elements["count"]
33
- if count_mapper
34
- if count_mapper["no_output"]
35
- result.delete("count")
36
- else
37
- result["count"] = result[count_mapper["target"]].size
38
- end
39
- end
40
-
41
- records_mapper = elements["records"]
42
- if records_mapper && result["records"]
43
- if records_mapper["no_output"]
44
- result.delete("records")
45
- else
46
- result["records"] = apply_output_range(result["records"], records_mapper)
47
- result["records"] = apply_output_attributes_and_format(result["records"], records_mapper)
48
- end
49
- end
50
- end
51
- output = output["output"]
52
- end
53
- emit(output, result)
54
- end
55
-
56
- def apply_output_attributes_and_format(items, output)
57
- attributes = output["attributes"] || []
58
- if output["format"] == "complex"
59
- items.collect! do |item|
60
- complex_item = {}
61
- attributes.each_with_index do |label, index|
62
- complex_item[label] = item[index]
63
- end
64
- complex_item
65
- end
66
- else
67
- items.collect! do |item|
68
- item[0...attributes.size]
69
- end
70
- end
71
- items
72
- end
73
-
74
- command :collector_search_reduce
75
- def collector_search_reduce(request)
76
- #XXX This is just a workaround. Errors should be handled by the framework itself.
77
- if input_name == "errors"
78
- return collector_reduce(request)
79
- end
80
-
81
- return unless request
82
- body[input_name].each do |output, elements|
83
- value = request
84
- old_value = output_values[output]
85
- value = reduce_elements(elements, old_value, request) if old_value
86
- emit(output, value)
87
- end
88
- end
89
-
90
- def reduce_elements(elements, left_values, right_values)
91
- result = {}
92
- elements.each do |key, deal|
93
- result[key] = reduce(deal, left_values[key], right_values[key])
94
- end
95
- result
96
- end
97
- end
98
- end
@@ -1,49 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013-2014 Droonga Project
4
- #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
8
- #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
-
18
- require "droonga/planner_plugin"
19
-
20
- module Droonga
21
- class CRUDPlanner < Droonga::PlannerPlugin
22
- repository.register("crud", self)
23
-
24
- command :add
25
- def add(message)
26
- scatter(message)
27
- end
28
-
29
- command :update
30
- def update(message)
31
- scatter(message)
32
- end
33
-
34
- # TODO: What is this?
35
- command :reset
36
- def reset(message)
37
- scatter(message)
38
- end
39
-
40
- private
41
- def scatter(message)
42
- super(message,
43
- :key => message["body"]["key"] || rand.to_s,
44
- :reduce => {
45
- "success" => "and"
46
- })
47
- end
48
- end
49
- end
@@ -1,393 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013-2014 Droonga Project
4
- #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
8
- #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
-
18
- require "droonga/searcher"
19
- require "droonga/distributed_command_planner"
20
-
21
- module Droonga
22
- class DistributedSearchPlanner < DistributedCommandPlanner
23
- def initialize(search_request_message)
24
- super
25
-
26
- @request = @source_message["body"]
27
- raise NoQuery.new unless @request
28
-
29
- @request = Marshal.load(Marshal.dump(@request))
30
- @queries = @request["queries"]
31
- end
32
-
33
- def plan
34
- raise Searcher::NoQuery.new if @queries.nil? || @queries.empty?
35
-
36
- Searcher::QuerySorter.validate_dependencies(@queries)
37
-
38
- ensure_unifiable!
39
-
40
- @queries.each do |input_name, query|
41
- transform_query(input_name, query)
42
- end
43
-
44
- @dataset = @source_message["dataset"] || @request["dataset"]
45
- broadcast(:body => @request)
46
-
47
- super
48
- end
49
-
50
- private
51
- UNLIMITED = -1
52
-
53
- def reduce_command
54
- "search_reduce"
55
- end
56
-
57
- def gather_command
58
- "search_gather"
59
- end
60
-
61
- def ensure_unifiable!
62
- @queries.each do |name, query|
63
- if unifiable?(name) && query["output"]
64
- query["output"]["unifiable"] = true
65
- end
66
- end
67
- end
68
-
69
- def unifiable?(name)
70
- query = @queries[name]
71
- return true if query["groupBy"]
72
- name = query["source"]
73
- return false unless @queries.keys.include?(name)
74
- unifiable?(name)
75
- end
76
-
77
- def transform_query(input_name, query)
78
- output = query["output"]
79
-
80
- # Skip reducing phase for a result with no output.
81
- if output.nil? or
82
- output["elements"].nil? or
83
- (!output["elements"].include?("count") &&
84
- !output["elements"].include?("records"))
85
- return
86
- end
87
-
88
- transformer = QueryTransformer.new(query)
89
-
90
- elements = transformer.mappers
91
- mapper = {}
92
- mapper["elements"] = elements unless elements.empty?
93
- reduce(input_name => { :reduce => transformer.reducers,
94
- :gather => mapper })
95
- end
96
-
97
- class QueryTransformer
98
- attr_reader :reducers, :mappers
99
-
100
- def initialize(query)
101
- @query = query
102
- @output = @query["output"]
103
- @reducers = {}
104
- @mappers = {}
105
- @output_records = true
106
- transform!
107
- end
108
-
109
- def transform!
110
- # The collector module supports only "simple" format search results.
111
- # So we have to override the format and restore it on the gathering
112
- # phase.
113
- @records_format = @output["format"] || "simple"
114
- if @output["format"] && @output["format"] != "simple"
115
- @output["format"] = "simple"
116
- end
117
-
118
- @sort_keys = @query["sortBy"] || []
119
- @sort_keys = @sort_keys["keys"] || [] if @sort_keys.is_a?(Hash)
120
-
121
- calculate_offset_and_limit!
122
- build_count_mapper_and_reducer!
123
- build_records_mapper_and_reducer!
124
- end
125
-
126
- def calculate_offset_and_limit!
127
- @original_sort_offset = sort_offset
128
- @original_output_offset = output_offset
129
- @original_sort_limit = sort_limit
130
- @original_output_limit = output_limit
131
-
132
- calculate_sort_offset!
133
- calculate_output_offset!
134
-
135
- # We have to calculate limit based on offset.
136
- # <A, B = limited integer (0...MAXINT)>
137
- # | sort limit | output limit | => | worker's sort limit | worker's output limit | final limit |
138
- # ============================= ====================================================================
139
- # | UNLIMITED | UNLIMITED | => | UNLIMITED | UNLIMITED | UNLIMITED |
140
- # | UNLIMITED | B | => | final_offset + B | final_offset + B | B |
141
- # | A | UNLIMITED | => | final_offset + A | final_offset + A | A |
142
- # | A | B | => | final_offset + max(A, B) | final_offset + min(A, B)| min(A, B) |
143
-
144
- # XXX final_limit and final_offset calculated in many times
145
-
146
- @records_offset = final_offset
147
- @records_limit = final_limit
148
-
149
- updated_sort_limit = nil
150
- updated_output_limit = nil
151
- if final_limit == UNLIMITED
152
- updated_output_limit = UNLIMITED
153
- else
154
- if rich_sort?
155
- updated_sort_limit = final_offset + [sort_limit, output_limit].max
156
- end
157
- updated_output_limit = final_offset + final_limit
158
- end
159
-
160
- if updated_sort_limit && updated_sort_limit != @query["sortBy"]["limit"]
161
- @query["sortBy"]["limit"] = updated_sort_limit
162
- end
163
- if updated_output_limit && @output["limit"] && updated_output_limit != @output["limit"]
164
- @output["limit"] = updated_output_limit
165
- end
166
- end
167
-
168
- def calculate_sort_offset!
169
- # Offset for workers must be zero, because we have to apply "limit" and
170
- # "offset" on the last gathering phase instead of each reducing phase.
171
- if rich_sort?
172
- @query["sortBy"]["offset"] = 0
173
- end
174
- end
175
-
176
- def sort_offset
177
- if rich_sort?
178
- @query["sortBy"]["offset"] || 0
179
- else
180
- 0
181
- end
182
- end
183
-
184
- def output_offset
185
- @output["offset"] || 0
186
- end
187
-
188
- def sort_limit
189
- if rich_sort?
190
- @query["sortBy"]["limit"] || UNLIMITED
191
- else
192
- UNLIMITED
193
- end
194
- end
195
-
196
- def output_limit
197
- @output["limit"] || 0
198
- end
199
-
200
- def calculate_output_offset!
201
- @output["offset"] = 0 if have_records? && @output["offset"]
202
- end
203
-
204
- def final_offset
205
- @original_sort_offset + @original_output_offset
206
- end
207
-
208
- def final_limit
209
- if @original_sort_limit == UNLIMITED && @original_output_limit == UNLIMITED
210
- UNLIMITED
211
- else
212
- if @original_sort_limit == UNLIMITED
213
- @original_output_limit
214
- elsif @original_output_limit == UNLIMITED
215
- @original_sort_limit
216
- else
217
- [@original_sort_limit, @original_output_limit].min
218
- end
219
- end
220
- end
221
-
222
- def have_records?
223
- @output["elements"].include?("records")
224
- end
225
-
226
- def rich_sort?
227
- @query["sortBy"].is_a?(Hash)
228
- end
229
-
230
- def unifiable?
231
- @output["unifiable"]
232
- end
233
-
234
- def build_count_mapper_and_reducer!
235
- return unless @output["elements"].include?("count")
236
-
237
- @reducers["count"] = {
238
- "type" => "sum",
239
- }
240
- if unifiable?
241
- @query["sortBy"]["limit"] = -1 if @query["sortBy"].is_a?(Hash)
242
- @output["limit"] = -1
243
- mapper = {
244
- "target" => "records",
245
- }
246
- unless @output["elements"].include?("records")
247
- @records_limit = -1
248
- @output["elements"] << "records"
249
- @output["attributes"] ||= ["_key"]
250
- @output_records = false
251
- end
252
- @mappers["count"] = mapper
253
- end
254
- end
255
-
256
- def build_records_mapper_and_reducer!
257
- # Skip reducing phase for a result with no record output.
258
- return if !@output["elements"].include?("records") || @records_limit.zero?
259
-
260
- # Append sort key attributes to the list of output attributes
261
- # temporarily, for the reducing phase. After all extra columns
262
- # are removed on the gathering phase.
263
- final_attributes = output_attribute_names
264
- update_output_attributes!
265
-
266
- @reducers["records"] = build_records_reducer
267
-
268
- mapper = {}
269
- if @output_records
270
- mapper["format"] = @records_format unless @records_format == "simple"
271
- mapper["attributes"] = final_attributes unless final_attributes.empty?
272
- mapper["offset"] = @records_offset unless @records_offset.zero?
273
- mapper["limit"] = @records_limit unless @records_limit.zero?
274
- else
275
- mapper["no_output"] = true
276
- end
277
- @mappers["records"] = mapper
278
- end
279
-
280
- def output_attribute_names
281
- attributes = @output["attributes"] || []
282
- if attributes.is_a?(Hash)
283
- attributes.keys
284
- else
285
- attributes.collect do |attribute|
286
- if attribute.is_a?(Hash)
287
- attribute["label"] || attribute["source"]
288
- else
289
- attribute
290
- end
291
- end
292
- end
293
- end
294
-
295
- def update_output_attributes!
296
- @output["attributes"] = array_style_attributes
297
- @output["attributes"] += sort_attribute_names
298
- if unifiable? && !source_column_names.include?("_key")
299
- @output["attributes"] << "_key"
300
- end
301
- end
302
-
303
- def array_style_attributes
304
- attributes = @output["attributes"] || []
305
- if attributes.is_a?(Hash)
306
- attributes.keys.collect do |key|
307
- attribute = attributes[key]
308
- case attribute
309
- when String
310
- {
311
- "label" => key,
312
- "source" => attribute,
313
- }
314
- when Hash
315
- attribute["label"] = key
316
- attribute
317
- end
318
- end
319
- else
320
- attributes
321
- end
322
- end
323
-
324
- def source_column_names
325
- attributes = @output["attributes"] || []
326
- if attributes.is_a?(Hash)
327
- attributes_hash = attributes
328
- attributes = []
329
- attributes_hash.each do |key, attribute|
330
- attributes << attribute["source"] || key
331
- end
332
- attributes
333
- else
334
- attributes.collect do |attribute|
335
- if attribute.is_a?(Hash)
336
- attribute["source"] || attribute["label"]
337
- else
338
- attribute
339
- end
340
- end
341
- end
342
- end
343
-
344
- def sort_attribute_names
345
- sort_attributes = @sort_keys.collect do |key|
346
- key = key[1..-1] if key[0] == "-"
347
- key
348
- end
349
- attributes = source_column_names
350
- sort_attributes.reject! do |attribute|
351
- attributes.include?(attribute)
352
- end
353
- sort_attributes
354
- end
355
-
356
- ASCENDING_OPERATOR = "<"
357
- DESCENDING_OPERATOR = ">"
358
-
359
- def build_records_reducer
360
- attributes = source_column_names
361
- key_column_index = attributes.index("_key")
362
-
363
- operators = @sort_keys.collect do |sort_key|
364
- operator = ASCENDING_OPERATOR
365
- if sort_key[0] == "-"
366
- operator = DESCENDING_OPERATOR
367
- sort_key = sort_key[1..-1]
368
- end
369
- {
370
- "operator" => operator,
371
- "column" => attributes.index(sort_key),
372
- }
373
- end
374
-
375
- reducer = {
376
- "type" => "sort",
377
- "operators" => operators,
378
- }
379
- if unifiable? && !key_column_index.nil?
380
- reducer["key_column"] = key_column_index
381
- end
382
-
383
- # On the reducing phase, we apply only "limit". We cannot apply
384
- # "offset" on this phase because the collector merges a pair of
385
- # results step by step even if there are three or more results.
386
- # Instead, we apply "offset" on the gathering phase.
387
- reducer["limit"] = @output["limit"]
388
-
389
- reducer
390
- end
391
- end
392
- end
393
- end