groonga-client 0.5.2 → 0.5.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 98c07600cda5089240101c97f1055cee06406515
4
- data.tar.gz: c26124ec4e3364a316949bb21bb9fbc00cac6661
3
+ metadata.gz: e5a1481041e35f70cc0bdfc67c42726260d4db93
4
+ data.tar.gz: '029977cb46c61d06fdb07d8d8ce71498c0375f87'
5
5
  SHA512:
6
- metadata.gz: d3aeb65b3107c978fdfef99efe64aadc4a5fd7c5ed4a4835c6377d2d1910acb795994e923ac2cd7bed0737d89740999e8a2c50f511ac762b2797593ad0be4e04
7
- data.tar.gz: dedc41cf9ce0f590bb557574d310d55a02cff6c033eec799ff30253093e18182c1141a9801eed0d8aee46df5c583b936511560d9905aabe673aaf2abaf3f78ef
6
+ metadata.gz: 5433daf65d59587e66d23850d27c3bec597e9fc81c501b65ff7ede45e21976d038f0241f757d1817d1b182f2c8d2aa5d4b74e9ff6ed91806c2c0ee7fe6ed9715
7
+ data.tar.gz: 4e9d0c4d4573587bdc4f0e06979aa5d9ae33bc81b6814a2a909066adc9990377d7ffba132908950645e0d0340a1310dca11ae4da8709516d482154324b91e3c1
data/bin/groonga-client CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # -*- ruby -*-
3
3
  #
4
- # Copyright (C) 2015-2016 Kouhei Sutou <kou@clear-code.com>
4
+ # Copyright (C) 2015-2017 Kouhei Sutou <kou@clear-code.com>
5
5
  #
6
6
  # This library is free software; you can redistribute it and/or
7
7
  # modify it under the terms of the GNU Lesser General Public
@@ -17,7 +17,7 @@
17
17
  # License along with this library; if not, write to the Free Software
18
18
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
 
20
- require "groonga/client/cli"
20
+ require "groonga/client/command-line/groonga-client"
21
21
 
22
- cli = Groonga::Client::CLI.new
23
- exit(cli.run(ARGV))
22
+ command_line = Groonga::Client::CommandLine::GroongaClient.new
23
+ exit(command_line.run(ARGV))
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- ruby -*-
3
+ #
4
+ # Copyright (C) 2017 Kouhei Sutou <kou@clear-code.com>
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License as published by the Free Software Foundation; either
9
+ # version 2.1 of the License, or (at your option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with this library; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
+
20
+ require "groonga/client/command-line/groonga-client-index-recreate"
21
+
22
+ command_line = Groonga::Client::CommandLine::GroongaClientIndexRecreate.new
23
+ exit(command_line.run(ARGV))
data/doc/text/news.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # NEWS
2
2
 
3
+ ## 0.5.3 - 2017-10-26
4
+
5
+ ### Improvements
6
+
7
+ * `Groonga::Client::Response::ColumnList::Column#flags`: Changed
8
+ return type to `Array<Strinng>` from `String`.
9
+
10
+ * `Groonga::Client::Response::ColumnList::Column#scalar?`: Added.
11
+
12
+ * `Groonga::Client::Response::ColumnList::Column#vector?`: Added.
13
+
14
+ * `Groonga::Client::Response::ColumnList::Column#index?`: Added.
15
+
16
+ * `Groonga::Client::Response::Schema::Command`: Added.
17
+
18
+ * `Groonga::Client::Response::Schema::Table#command`: Changed return
19
+ type to `Hash` to `Groonga::Client::Response::Schema::Command`.
20
+
21
+ * `Groonga::Client::Response::Schema::Column#command`: Changed return
22
+ type to `Hash` to `Groonga::Client::Response::Schema::Command`.
23
+
24
+ * `Groonga::Client::Response::Schema#[]`: Added.
25
+
26
+ * `groonga-client-index-recreate`: Added a new command that
27
+ recreates indexes dynamically and safely.
28
+
3
29
  ## 0.5.2 - 2017-09-27
4
30
 
5
31
  ### Improvements
@@ -0,0 +1,325 @@
1
+ # Copyright (C) 2017 Kouhei Sutou <kou@clear-code.com>
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 as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "optparse"
18
+ require "json"
19
+
20
+ require "groonga/client"
21
+
22
+ module Groonga
23
+ class Client
24
+ module CommandLine
25
+ class GroongaClientIndexRecreate
26
+ def initialize
27
+ @url = nil
28
+ @protocol = :http
29
+ @host = "localhost"
30
+ @port = nil
31
+
32
+ @read_timeout = -1
33
+
34
+ @interval = :day
35
+
36
+ @n_workers = 0
37
+ end
38
+
39
+ def run(argv)
40
+ target_indexes = parse_command_line(argv)
41
+
42
+ Client.open(:url => @url,
43
+ :protocol => @protocol,
44
+ :host => @host,
45
+ :port => @port,
46
+ :read_timeout => @read_timeout,
47
+ :backend => :synchronous) do |client|
48
+ runner = Runner.new(client, @interval, target_indexes)
49
+ runner.run do
50
+ @n_workers.times do
51
+ client.database_unmap
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+ def parse_command_line(argv)
59
+ parser = OptionParser.new
60
+ parser.version = VERSION
61
+ parser.banner += " LEXICON1.INDEX1 LEXICON2.INDEX2 ..."
62
+
63
+ parser.separator("")
64
+ parser.separator("Connection:")
65
+
66
+ parser.on("--url=URL",
67
+ "URL to connect to Groonga server.",
68
+ "If this option is specified,",
69
+ "--protocol, --host and --port are ignored.") do |url|
70
+ @url = url
71
+ end
72
+
73
+ available_protocols = [:http, :gqtp]
74
+ parser.on("--protocol=PROTOCOL", [:http, :gqtp],
75
+ "Protocol to connect to Groonga server.",
76
+ "[#{available_protocols.join(", ")}]",
77
+ "(#{@protocol})") do |protocol|
78
+ @protocol = protocol
79
+ end
80
+
81
+ parser.on("--host=HOST",
82
+ "Groonga server to be connected.",
83
+ "(#{@host})") do |host|
84
+ @host = host
85
+ end
86
+
87
+ parser.on("--port=PORT", Integer,
88
+ "Port number of Groonga server to be connected.",
89
+ "(auto)") do |port|
90
+ @port = port
91
+ end
92
+
93
+ parser.on("--read-timeout=TIMEOUT", Integer,
94
+ "Timeout on reading response from Groonga server.",
95
+ "You can disable timeout by specifying -1.",
96
+ "(#{@read_timeout})") do |timeout|
97
+ @read_timeout = timeout
98
+ end
99
+
100
+ parser.separator("")
101
+ parser.separator("Configuration:")
102
+
103
+ available_intervals = [:day]
104
+ parser.on("--interval=INTERVAL", available_intervals,
105
+ "Index create interval.",
106
+ "[#{available_intervals.join(", ")}]",
107
+ "(#{@interval})") do |interval|
108
+ @interval = interval
109
+ end
110
+
111
+ parser.separator("")
112
+ parser.separator("groonga-httpd:")
113
+
114
+ parser.on("--n-workers=N", Integer,
115
+ "The number of groonga-httpd workers.",
116
+ "This options is meaningless for groonga -s.",
117
+ "(#{@n_workers})") do |n|
118
+ @n_workers = n
119
+ end
120
+
121
+ target_indexes = parser.parse(argv)
122
+
123
+ @port ||= default_port(@protocol)
124
+
125
+ target_indexes
126
+ end
127
+
128
+ def default_port(protocol)
129
+ case protocol
130
+ when :http
131
+ 10041
132
+ when :gqtp
133
+ 10043
134
+ end
135
+ end
136
+
137
+ class Runner
138
+ def initialize(client, interval, target_indexes)
139
+ @client = client
140
+ @interval = interval
141
+ @target_indexes = target_indexes
142
+ @now = Time.now
143
+ end
144
+
145
+ def run
146
+ catch do |tag|
147
+ @abort_tag = tag
148
+ alias_column = ensure_alias_column
149
+ @target_indexes.each do |index|
150
+ current_index = recreate_index(index, alias_column)
151
+ remove_old_indexes(index, current_index)
152
+ end
153
+ yield if block_given?
154
+ true
155
+ end
156
+ end
157
+
158
+ private
159
+ def abort_run(message)
160
+ $stderr.puts(message)
161
+ throw(@abort_tag, false)
162
+ end
163
+
164
+ def execute_command(name, arguments={})
165
+ response = @client.execute(name, arguments)
166
+ unless response.success?
167
+ abort_run("Failed to run #{name}: #{response.inspect}")
168
+ end
169
+ response
170
+ end
171
+
172
+ def config_get(key)
173
+ execute_command(:config_get, :key => key).body
174
+ end
175
+
176
+ def config_set(key, value)
177
+ execute_command(:config_set, :key => key, :value => value).body
178
+ end
179
+
180
+ def object_exist?(name)
181
+ execute_command(:object_exist, :name => name).body
182
+ end
183
+
184
+ def column_rename(table, name, new_name)
185
+ execute_command(:column_rename,
186
+ :table => table,
187
+ :name => name,
188
+ :new_name => new_name).body
189
+ end
190
+
191
+ def column_list(table)
192
+ execute_command(:column_list, :table => table)
193
+ end
194
+
195
+ def column_remove(table, column)
196
+ execute_command(:column_remove,
197
+ :table => table,
198
+ :name => column)
199
+ end
200
+
201
+ def column_create_similar(table, column_name, base_column_name)
202
+ info = execute_command(:schema)["#{table}.#{base_column_name}"]
203
+ arguments = info.command.arguments.merge("name" => column_name)
204
+ execute_command(:column_create, arguments).body
205
+ end
206
+
207
+ def set_alias(alias_column, alias_name, real_name)
208
+ table, column = alias_column.split(".", 2)
209
+ values = [
210
+ {
211
+ "_key" => alias_name,
212
+ column => real_name,
213
+ },
214
+ ]
215
+ response = execute_command(:load,
216
+ :table => table,
217
+ :values => JSON.generate(values),
218
+ :command_version => "3",
219
+ :output_errors => "yes")
220
+ response.errors.each do |error|
221
+ unless error.return_code.zero?
222
+ abort_run("Failed to set alias: " +
223
+ "<#{alias_name}> -> <#{real_name}>: " +
224
+ "#{error.message}(#{error.return_code})")
225
+ end
226
+ end
227
+ end
228
+
229
+ def resolve_alias(alias_column, key)
230
+ table, column = alias_column.split(".", 2)
231
+ filter = "_key == #{ScriptSyntax.format_string(key)}"
232
+ response = execute_command(:select,
233
+ :table => table,
234
+ :filter => filter,
235
+ :output_columns => column)
236
+ return nil if response.n_hits.zero?
237
+ response.records.first[column]
238
+ end
239
+
240
+ def ensure_alias_column
241
+ alias_column = config_get("alias.column")
242
+ if alias_column.empty?
243
+ table = "Aliases"
244
+ column = "real_name"
245
+ alias_column = "#{table}.#{column}"
246
+ unless object_exist?(table)
247
+ execute_command(:table_create,
248
+ :name => table,
249
+ :flags => "TABLE_HASH_KEY",
250
+ :key_type => "ShortText")
251
+ end
252
+ unless object_exist?(alias_column)
253
+ execute_command(:column_create,
254
+ :table => table,
255
+ :name => column,
256
+ :flags => "COLUMN_SCALAR",
257
+ :type => "ShortText")
258
+ end
259
+ config_set("alias.column", alias_column)
260
+ end
261
+ alias_column
262
+ end
263
+
264
+ def recreate_index(full_index_name, alias_column)
265
+ revision = generate_revision
266
+ table_name, index_name = full_index_name.split(".", 2)
267
+ real_index_name = "#{index_name}_#{revision}"
268
+ real_full_index_name = "#{table_name}.#{real_index_name}"
269
+ if object_exist?(full_index_name)
270
+ set_alias(alias_column, full_index_name, real_full_index_name)
271
+ column_rename(table_name, index_name, real_index_name)
272
+ nil
273
+ elsif object_exist?(real_full_index_name)
274
+ nil
275
+ else
276
+ full_current_index_name =
277
+ resolve_alias(alias_column, full_index_name)
278
+ current_table_name, current_index_name =
279
+ full_current_index_name.split(".", 2)
280
+ if current_table_name != table_name
281
+ abort_run("Different lexicon isn't supported: " +
282
+ "<#{full_index_name}> -> <#{full_current_index_name}>")
283
+ end
284
+ if current_index_name == real_index_name
285
+ abort_run("Alias doesn't specify real index column: " +
286
+ "<#{full_current_index_name}>")
287
+ end
288
+ column_create_similar(table_name,
289
+ real_index_name,
290
+ current_index_name)
291
+ set_alias(alias_column, full_index_name, real_full_index_name)
292
+ full_current_index_name
293
+ end
294
+ end
295
+
296
+ def remove_old_indexes(full_base_index_name, full_current_index_name)
297
+ return if full_current_index_name.nil?
298
+
299
+ table_name, base_index_name = full_base_index_name.split(".", 2)
300
+ _, current_index_name = full_current_index_name.split(".", 2)
301
+
302
+ target_index_columns = column_list(table_name).find_all do |column|
303
+ column.name.start_with?("#{base_index_name}_") and
304
+ column.index?
305
+ end
306
+ target_index_columns.collect(&:name).sort.each do |index_name|
307
+ next unless /_(\d{4})(\d{2})(\d{2})\z/ =~ index_name
308
+ next if index_name >= current_index_name
309
+ column_remove(table_name, index_name)
310
+ end
311
+ end
312
+
313
+ def generate_revision
314
+ case @interval
315
+ when :day
316
+ @now.strftime("%Y%m%d")
317
+ else
318
+ abort_run("Unsupported revision: #{@interval}")
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,244 @@
1
+ # Copyright (C) 2015-2017 Kouhei Sutou <kou@clear-code.com>
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 as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "optparse"
18
+ require "json"
19
+ require "securerandom"
20
+
21
+ require "groonga/client"
22
+
23
+ require "groonga/command/parser"
24
+
25
+ module Groonga
26
+ class Client
27
+ module CommandLine
28
+ class GroongaClient
29
+ def initialize
30
+ @url = nil
31
+ @protocol = :http
32
+ @host = "localhost"
33
+ @port = nil
34
+
35
+ @read_timeout = Client::Default::READ_TIMEOUT
36
+
37
+ @chunk = false
38
+
39
+ @runner_options = {
40
+ :split_load_chunk_size => 10000,
41
+ :generate_request_id => false,
42
+ }
43
+ end
44
+
45
+ def run(argv)
46
+ command_file_paths = parse_command_line(argv)
47
+
48
+ Client.open(:url => @url,
49
+ :protocol => @protocol,
50
+ :host => @host,
51
+ :port => @port,
52
+ :read_timeout => @read_timeout,
53
+ :chunk => @chunk,
54
+ :backend => :synchronous) do |client|
55
+ runner = Runner.new(client, @runner_options)
56
+
57
+ if command_file_paths.empty?
58
+ $stdin.each_line do |line|
59
+ runner << line
60
+ end
61
+ else
62
+ command_file_paths.each do |command_file_path|
63
+ File.open(command_file_path) do |command_file|
64
+ last_line = nil
65
+ command_file.each_line do |line|
66
+ last_line = line
67
+ runner << line
68
+ end
69
+ if last_line and !last_line.end_with?("\n")
70
+ runner << "\n"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ runner.finish
76
+ end
77
+
78
+ true
79
+ end
80
+
81
+ private
82
+ def parse_command_line(argv)
83
+ parser = OptionParser.new
84
+ parser.version = VERSION
85
+ parser.banner += " GROONGA_COMMAND_FILE1 GROONGA_COMMAND_FILE2 ..."
86
+
87
+ parser.separator("")
88
+
89
+ parser.separator("Connection:")
90
+
91
+ parser.on("--url=URL",
92
+ "URL to connect to Groonga server.",
93
+ "If this option is specified,",
94
+ "--protocol, --host and --port are ignored.") do |url|
95
+ @url = url
96
+ end
97
+
98
+ available_protocols = [:http, :gqtp]
99
+ parser.on("--protocol=PROTOCOL", [:http, :gqtp],
100
+ "Protocol to connect to Groonga server.",
101
+ "[#{available_protocols.join(", ")}]",
102
+ "(#{@protocol})") do |protocol|
103
+ @protocol = protocol
104
+ end
105
+
106
+ parser.on("--host=HOST",
107
+ "Groonga server to be connected.",
108
+ "(#{@host})") do |host|
109
+ @host = host
110
+ end
111
+
112
+ parser.on("--port=PORT", Integer,
113
+ "Port number of Groonga server to be connected.",
114
+ "(auto)") do |port|
115
+ @port = port
116
+ end
117
+
118
+ parser.on("--read-timeout=TIMEOUT", Integer,
119
+ "Timeout on reading response from Groonga server.",
120
+ "You can disable timeout by specifying -1.",
121
+ "(#{@read_timeout})") do |timeout|
122
+ @read_timeout = timeout
123
+ end
124
+
125
+ parser.on("--split-load-chunk-size=SIZE", Integer,
126
+ "Split a large load to small loads.",
127
+ "Each small load has at most SIZE records.",
128
+ "Set 0 to SIZE to disable this feature.",
129
+ "(#{@runner_options[:split_load_chunk_size]})") do |size|
130
+ @runner_options[:split_load_chunk_size] = size
131
+ end
132
+
133
+ parser.on("--[no-]generate-request-id",
134
+ "Add auto generated request ID to all commands.",
135
+ "(#{@runner_options[:generate_request_id]})") do |boolean|
136
+ @runner_options[:generate_request_id] = boolean
137
+ end
138
+
139
+ parser.on("--[no-]chunk",
140
+ "Use \"Transfer-Encoding: chunked\" for load command.",
141
+ "HTTP only.",
142
+ "(#{@chunk})") do |boolean|
143
+ @chunk = boolean
144
+ end
145
+
146
+ command_file_paths = parser.parse(argv)
147
+
148
+ @port ||= default_port(@protocol)
149
+
150
+ command_file_paths
151
+ end
152
+
153
+ def default_port(protocol)
154
+ case protocol
155
+ when :http
156
+ 10041
157
+ when :gqtp
158
+ 10043
159
+ end
160
+ end
161
+
162
+ class Runner
163
+ def initialize(client, options={})
164
+ @client = client
165
+ @split_load_chunk_size = options[:split_load_chunk_size] || 10000
166
+ @generate_request_id = options[:generate_request_id]
167
+ @load_values = []
168
+ @parser = create_command_parser
169
+ end
170
+
171
+ def <<(line)
172
+ @parser << line
173
+ end
174
+
175
+ def finish
176
+ @parser.finish
177
+ end
178
+
179
+ private
180
+ def create_command_parser
181
+ parser = Groonga::Command::Parser.new
182
+
183
+ parser.on_command do |command|
184
+ run_command(command)
185
+ end
186
+
187
+ parser.on_load_columns do |command, columns|
188
+ command[:columns] ||= columns.join(",")
189
+ end
190
+
191
+ parser.on_load_value do |command, value|
192
+ unless command[:values]
193
+ @load_values << value
194
+ if @load_values.size == @split_load_chunk_size
195
+ consume_load_values(command)
196
+ end
197
+ end
198
+ command.original_source.clear
199
+ end
200
+
201
+ parser.on_load_complete do |command|
202
+ if command[:values]
203
+ run_command(command)
204
+ else
205
+ consume_load_values(command)
206
+ end
207
+ end
208
+
209
+ parser
210
+ end
211
+
212
+ def consume_load_values(load_command)
213
+ return if @load_values.empty?
214
+
215
+ values_json = "["
216
+ @load_values.each_with_index do |value, i|
217
+ values_json << "," unless i.zero?
218
+ values_json << "\n"
219
+ values_json << JSON.generate(value)
220
+ end
221
+ values_json << "\n]\n"
222
+ load_command[:values] = values_json
223
+ run_command(load_command)
224
+ @load_values.clear
225
+ load_command[:values] = nil
226
+ end
227
+
228
+ def run_command(command)
229
+ command[:request_id] ||= SecureRandom.uuid if @generate_request_id
230
+ response = @client.execute(command)
231
+ case command.output_type
232
+ when :json
233
+ puts(JSON.pretty_generate([response.header, response.body]))
234
+ when :xml
235
+ puts(response.raw)
236
+ else
237
+ puts(response.body)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end