groonga-client 0.5.2 → 0.5.3

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