fluent-plugin-droonga 0.9.9 → 1.0.0

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