teradata-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/COPYING +515 -0
- data/Gemfile +4 -0
- data/README.md +47 -0
- data/Rakefile +1 -0
- data/examples/query.rb +38 -0
- data/examples/show-queryband.rb +26 -0
- data/examples/tu/excel/excel.rb +86 -0
- data/examples/tu/excel/fill.rb +94 -0
- data/examples/tu/excel/template.xls +0 -0
- data/examples/tu/tusample1.rb +6 -0
- data/examples/tu/tusample2.rb +7 -0
- data/examples/tu/web/bitdao.rb +197 -0
- data/examples/tu/web/bitdao/teradata.rb +23 -0
- data/examples/tu/web/bitweb.rb +575 -0
- data/examples/tu/web/messages +0 -0
- data/examples/tu/web/server.rb +94 -0
- data/examples/tu/web/tdwalker.rb +42 -0
- data/examples/tu/web/template/database/show +56 -0
- data/examples/tu/web/template/footer +2 -0
- data/examples/tu/web/template/header +7 -0
- data/examples/update.rb +31 -0
- data/ext/teradata/cli/cli.c +363 -0
- data/ext/teradata/cli/extconf.rb +20 -0
- data/lib/teradata.rb +14 -0
- data/lib/teradata/cli.rb +4 -0
- data/lib/teradata/cli/version.rb +5 -0
- data/lib/teradata/connection.rb +1125 -0
- data/lib/teradata/dbobject.rb +453 -0
- data/lib/teradata/exception.rb +15 -0
- data/lib/teradata/utils.rb +184 -0
- data/teradata-cli.gemspec +24 -0
- data/test/all +7 -0
- data/test/rubyclitestutils.rb +99 -0
- data/test/test_connection.rb +298 -0
- data/test/test_dbobject.rb +153 -0
- data/test/test_record.rb +210 -0
- metadata +115 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
def extconf_main
|
4
|
+
$objs = %w(cli.o)
|
5
|
+
dir_config 'cli'
|
6
|
+
if have_library cliv2_libname
|
7
|
+
create_makefile 'teradata/cli'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def cliv2_libname
|
12
|
+
case RUBY_PLATFORM
|
13
|
+
when /mswin32|mingw/ then 'wincli32'
|
14
|
+
when /mswin64/ then 'wincli64'
|
15
|
+
else
|
16
|
+
'cliv2'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
extconf_main
|
data/lib/teradata.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#
|
2
|
+
# $Id: teradata.rb 7 2010-03-04 16:54:09Z tdaoki $
|
3
|
+
#
|
4
|
+
# Copyright (C) 2009,2010 Teradata Japan, LTD.
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU LGPL2, Lesser General Public License version 2.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'teradata/connection'
|
12
|
+
require 'teradata/dbobject'
|
13
|
+
require 'teradata/utils'
|
14
|
+
require 'teradata/exception'
|
data/lib/teradata/cli.rb
ADDED
@@ -0,0 +1,1125 @@
|
|
1
|
+
#
|
2
|
+
# $Id: connection.rb 7 2010-03-04 16:54:09Z tdaoki $
|
3
|
+
#
|
4
|
+
# Copyright (C) 2009,2010 Teradata Japan, LTD.
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU LGPL2, Lesser General Public License version 2.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'teradata/cli'
|
12
|
+
require 'teradata/utils'
|
13
|
+
require 'teradata/exception'
|
14
|
+
require 'forwardable'
|
15
|
+
require 'stringio'
|
16
|
+
|
17
|
+
module Teradata
|
18
|
+
|
19
|
+
class ConnectionError < CLIError; end
|
20
|
+
class MetaDataFormatError < CLIError; end
|
21
|
+
|
22
|
+
class SQLError < CLIError
|
23
|
+
def initialize(code, info, message)
|
24
|
+
super message
|
25
|
+
@code = code
|
26
|
+
@info = info
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :code
|
30
|
+
attr_reader :info
|
31
|
+
end
|
32
|
+
|
33
|
+
class UserAbort < SQLError; end
|
34
|
+
|
35
|
+
def Teradata.connect(*args, &block)
|
36
|
+
Connection.open(*args, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Connection
|
40
|
+
|
41
|
+
class << self
|
42
|
+
alias open new
|
43
|
+
end
|
44
|
+
|
45
|
+
def Connection.default_session_charset
|
46
|
+
Teradata::SessionCharset.new('UTF8')
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(logon_string, options = {})
|
50
|
+
session_charset = options[:session_charset] || Connection.default_session_charset
|
51
|
+
internal_encoding = options[:internal_encoding] || default_internal_encoding()
|
52
|
+
@logger = options[:logger] || NullLogger.new
|
53
|
+
@logon_string = Teradata::LogonString.intern(logon_string)
|
54
|
+
@session_charset = Teradata::SessionCharset.intern(session_charset)
|
55
|
+
@external_encoding = @session_charset.encoding
|
56
|
+
@internal_encoding = internal_encoding
|
57
|
+
ex = StringExtractor.get(@external_encoding, @internal_encoding)
|
58
|
+
log { "session charset = #{@session_charset}" }
|
59
|
+
log { "external encoding = #{@external_encoding}" }
|
60
|
+
log { "internal encoding = #{@internal_encoding}" }
|
61
|
+
log { "logon... (#{@logon_string.safe_string})" }
|
62
|
+
@cli = CLI.new(logon_string.to_s, @session_charset.name)
|
63
|
+
log { "logon succeeded" }
|
64
|
+
@cli.string_extractor = ex
|
65
|
+
@cli.logger = @logger
|
66
|
+
if block_given?
|
67
|
+
begin
|
68
|
+
yield self
|
69
|
+
ensure
|
70
|
+
close unless closed?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if defined?(::Encoding)
|
76
|
+
def default_internal_encoding
|
77
|
+
Encoding.default_internal
|
78
|
+
end
|
79
|
+
else
|
80
|
+
def default_internal_encoding
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
private :default_internal_encoding
|
85
|
+
|
86
|
+
class NullLogger
|
87
|
+
def debug(*args) end
|
88
|
+
def info(*args) end
|
89
|
+
def warn(*args) end
|
90
|
+
def error(*args) end
|
91
|
+
def fatal(*args) end
|
92
|
+
def unknown(*args) end
|
93
|
+
def close(*args) end
|
94
|
+
def log(*args) end
|
95
|
+
def add(*args) end
|
96
|
+
def <<(*args) end
|
97
|
+
def level=(*args) end
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_reader :logon_string
|
101
|
+
attr_reader :external_encoding
|
102
|
+
attr_reader :internal_encoding
|
103
|
+
|
104
|
+
def inspect
|
105
|
+
"\#<#{self.class} #{@logon_string.safe_string}>"
|
106
|
+
end
|
107
|
+
|
108
|
+
if defined?(::Encoding) # M17N enabled
|
109
|
+
|
110
|
+
class StringExtractor
|
111
|
+
class NoConversion
|
112
|
+
def initialize(external)
|
113
|
+
@external = external
|
114
|
+
end
|
115
|
+
|
116
|
+
def extract(str)
|
117
|
+
str.force_encoding @external
|
118
|
+
str
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def StringExtractor.get(external, internal)
|
123
|
+
internal ? new(external, internal) : NoConversion.new(external)
|
124
|
+
end
|
125
|
+
|
126
|
+
def initialize(external, internal)
|
127
|
+
@external = external
|
128
|
+
@converter = Encoding::Converter.new(external, internal)
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract(str)
|
132
|
+
str.force_encoding @external
|
133
|
+
@converter.convert(str)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
else # no M17N: Ruby 1.8
|
138
|
+
|
139
|
+
class StringExtractor
|
140
|
+
def StringExtractor.get(external, internal)
|
141
|
+
raise ArgumentError, "encoding conversion is not supported on Ruby 1.8" if internal
|
142
|
+
new()
|
143
|
+
end
|
144
|
+
|
145
|
+
def extract(str)
|
146
|
+
str
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
def execute_update(sql)
|
153
|
+
log { "[UPD] #{sql}" }
|
154
|
+
@cli.request canonicalize(sql)
|
155
|
+
begin
|
156
|
+
rs = @cli.read_result_set
|
157
|
+
rs.value_all
|
158
|
+
return rs
|
159
|
+
ensure
|
160
|
+
close_request
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
alias update execute_update
|
165
|
+
|
166
|
+
def execute_query(sql)
|
167
|
+
log { "[SEL] #{sql}" }
|
168
|
+
@cli.request canonicalize(sql)
|
169
|
+
begin
|
170
|
+
rs = @cli.read_result_set
|
171
|
+
rs.value
|
172
|
+
if block_given?
|
173
|
+
yield rs
|
174
|
+
else
|
175
|
+
rs.fetch_all
|
176
|
+
end
|
177
|
+
ensure
|
178
|
+
close_request
|
179
|
+
end
|
180
|
+
rs
|
181
|
+
end
|
182
|
+
|
183
|
+
alias query execute_query
|
184
|
+
|
185
|
+
def canonicalize(sql)
|
186
|
+
s = sql.gsub(/\r?\n/, "\r")
|
187
|
+
@external_encoding ? s.encode(@external_encoding) : s
|
188
|
+
end
|
189
|
+
private :canonicalize
|
190
|
+
|
191
|
+
def entries(sql)
|
192
|
+
execute_query(sql).entries
|
193
|
+
end
|
194
|
+
|
195
|
+
def transaction
|
196
|
+
aborting = false
|
197
|
+
begin_transaction
|
198
|
+
begin
|
199
|
+
yield
|
200
|
+
rescue UserAbort => err
|
201
|
+
aborting = true
|
202
|
+
raise err
|
203
|
+
ensure
|
204
|
+
if $@
|
205
|
+
begin
|
206
|
+
abort unless aborting
|
207
|
+
rescue UserAbort # do not override original exception
|
208
|
+
end
|
209
|
+
else
|
210
|
+
end_transaction
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def begin_transaction
|
216
|
+
execute_update "BEGIN TRANSACTION"
|
217
|
+
end
|
218
|
+
|
219
|
+
def end_transaction
|
220
|
+
execute_update "END TRANSACTION"
|
221
|
+
end
|
222
|
+
|
223
|
+
def abort
|
224
|
+
execute_update "ABORT"
|
225
|
+
end
|
226
|
+
|
227
|
+
def drop(obj)
|
228
|
+
execute_update "DROP #{obj.type_name} #{obj.name};"
|
229
|
+
end
|
230
|
+
|
231
|
+
def info
|
232
|
+
recs = entries("HELP SESSION")
|
233
|
+
unless recs.size == 1
|
234
|
+
raise "HELP SESSION did not return 1 record??? size=#{recs.size}"
|
235
|
+
end
|
236
|
+
SessionInfo.for_record(recs.first)
|
237
|
+
end
|
238
|
+
|
239
|
+
# :nodoc: internal use only
|
240
|
+
def close_request
|
241
|
+
@cli.skip_current_request
|
242
|
+
debug { "CLI.end_request" }
|
243
|
+
@cli.end_request
|
244
|
+
end
|
245
|
+
|
246
|
+
def close
|
247
|
+
log { "logoff..." }
|
248
|
+
debug { "CLI.logoff" }
|
249
|
+
@cli.logoff
|
250
|
+
log { "logoff succeeded" }
|
251
|
+
end
|
252
|
+
|
253
|
+
def closed?
|
254
|
+
not @cli.logging_on?
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def log(&block)
|
260
|
+
@logger.info { "#{id_string}: #{yield}" }
|
261
|
+
end
|
262
|
+
|
263
|
+
def debug(&block)
|
264
|
+
@logger.debug { "#{id_string}: #{yield}" }
|
265
|
+
end
|
266
|
+
|
267
|
+
def id_string
|
268
|
+
"Teradata::Connection:#{'%x' % object_id}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
class CLI # reopen
|
274
|
+
|
275
|
+
attr_accessor :string_extractor
|
276
|
+
attr_accessor :logger
|
277
|
+
|
278
|
+
def request(sql)
|
279
|
+
@eor = false # EndOfRequest
|
280
|
+
send_request sql
|
281
|
+
end
|
282
|
+
|
283
|
+
# == Non-Valued Result CLI Response Sequence
|
284
|
+
#
|
285
|
+
# PclSUCCESS
|
286
|
+
# PclENDSTATEMENT
|
287
|
+
# PclSUCCESS
|
288
|
+
# PclENDSTATEMENT
|
289
|
+
# ...
|
290
|
+
# PclENDREQUEST
|
291
|
+
#
|
292
|
+
# == Valued Result CLI Response Sequence
|
293
|
+
#
|
294
|
+
# === On Success
|
295
|
+
#
|
296
|
+
# PclSUCCESS
|
297
|
+
# PclPREPINFO
|
298
|
+
# PclDATAINFO
|
299
|
+
# PclRECORD
|
300
|
+
# PclRECORD
|
301
|
+
# ...
|
302
|
+
# PclENDSTATEMENT
|
303
|
+
#
|
304
|
+
# PclSUCCESS
|
305
|
+
# PclPREPINFO
|
306
|
+
# PclDATAINFO
|
307
|
+
# PclRECORD
|
308
|
+
# PclRECORD
|
309
|
+
# ...
|
310
|
+
# PclENDSTATEMENT
|
311
|
+
#
|
312
|
+
# PclENDREQUEST
|
313
|
+
#
|
314
|
+
# == CLI Response Sequence on Failure
|
315
|
+
#
|
316
|
+
# PclSUCCESS
|
317
|
+
# PclENDSTATEMENT
|
318
|
+
# ...
|
319
|
+
# PclFAILURE
|
320
|
+
|
321
|
+
def read_result_set
|
322
|
+
each_fet_parcel do |parcel|
|
323
|
+
case parcel.flavor_name
|
324
|
+
when 'PclSUCCESS', 'PclFAILURE', 'PclERROR'
|
325
|
+
return ResultSet.new(parcel.sql_status, self)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
nil
|
329
|
+
end
|
330
|
+
|
331
|
+
def read_metadata
|
332
|
+
each_fet_parcel do |parcel|
|
333
|
+
case parcel.flavor_name
|
334
|
+
when 'PclPREPINFO'
|
335
|
+
meta = MetaData.parse_prepinfo(parcel.data, string_extractor())
|
336
|
+
debug { "metadata = #{meta.inspect}" }
|
337
|
+
return meta
|
338
|
+
when 'PclDATAINFO'
|
339
|
+
when 'PclENDSTATEMENT'
|
340
|
+
# null request returns no metadata.
|
341
|
+
return nil
|
342
|
+
else
|
343
|
+
;
|
344
|
+
end
|
345
|
+
end
|
346
|
+
warn { "read_metadata: each_fet_parcel returned before PclENDSTATEMENT?" }
|
347
|
+
nil # FIXME: should raise?
|
348
|
+
end
|
349
|
+
|
350
|
+
def read_record
|
351
|
+
each_fet_parcel do |parcel|
|
352
|
+
case parcel.flavor_name
|
353
|
+
when 'PclRECORD'
|
354
|
+
return parcel.data
|
355
|
+
when 'PclENDSTATEMENT'
|
356
|
+
return nil
|
357
|
+
else
|
358
|
+
;
|
359
|
+
end
|
360
|
+
end
|
361
|
+
warn { "read_record: each_fet_parcel returned before PclENDSTATEMENT?" }
|
362
|
+
nil # FIXME: should raise?
|
363
|
+
end
|
364
|
+
|
365
|
+
def skip_current_statement
|
366
|
+
each_fet_parcel do |parcel|
|
367
|
+
case parcel.flavor_name
|
368
|
+
when 'PclENDSTATEMENT'
|
369
|
+
return
|
370
|
+
end
|
371
|
+
end
|
372
|
+
# each_fet_parcel returns before PclENDSTATEMENT when error occured
|
373
|
+
end
|
374
|
+
|
375
|
+
def skip_current_request
|
376
|
+
each_fet_parcel do |parcel|
|
377
|
+
;
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def each_fet_parcel
|
382
|
+
return if @eor
|
383
|
+
while true
|
384
|
+
debug { "CLI.fetch" }
|
385
|
+
fetch
|
386
|
+
flavor = flavor_name()
|
387
|
+
debug { "fetched: #{flavor}" }
|
388
|
+
case flavor
|
389
|
+
when 'PclENDREQUEST'
|
390
|
+
debug { "=== End Request ===" }
|
391
|
+
@eor = true
|
392
|
+
return
|
393
|
+
when 'PclFAILURE', 'PclERROR'
|
394
|
+
@eor = true
|
395
|
+
end
|
396
|
+
yield FetchedParcel.new(flavor, self)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
private
|
401
|
+
|
402
|
+
def warn(&block)
|
403
|
+
@logger.warn { "Teradata::CLI:#{'%x' % object_id}: #{yield}" }
|
404
|
+
end
|
405
|
+
|
406
|
+
def debug(&block)
|
407
|
+
@logger.debug { "Teradata::CLI:#{'%x' % object_id}: #{yield}" }
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
class FetchedParcel
|
414
|
+
|
415
|
+
def initialize(flavor_name, cli)
|
416
|
+
@flavor_name = flavor_name
|
417
|
+
@cli = cli
|
418
|
+
end
|
419
|
+
|
420
|
+
attr_reader :flavor_name
|
421
|
+
|
422
|
+
def message
|
423
|
+
@cli.message
|
424
|
+
end
|
425
|
+
|
426
|
+
def data
|
427
|
+
@cli.data
|
428
|
+
end
|
429
|
+
|
430
|
+
def sql_status
|
431
|
+
case @flavor_name
|
432
|
+
when 'PclSUCCESS' then SuccessStatus.parse(@cli.data)
|
433
|
+
when 'PclFAILURE' then FailureStatus.parse(@cli.data)
|
434
|
+
when 'PclERROR' then ErrorStatus.parse(@cli.data)
|
435
|
+
else
|
436
|
+
raise "must not happen: \#sql_status called for flavor #{@flavor_name}"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
end
|
441
|
+
|
442
|
+
|
443
|
+
class SuccessStatus
|
444
|
+
|
445
|
+
def SuccessStatus.parse(parcel_data)
|
446
|
+
stmt_no, _, act_cnt, warn_code, n_fields, act_type, warn_len = parcel_data.unpack('CCLSSSS')
|
447
|
+
warning = parcel_data[13, warn_len]
|
448
|
+
new(stmt_no, act_cnt, warn_code, n_fields, act_type, warning)
|
449
|
+
end
|
450
|
+
|
451
|
+
def initialize(stmt_no, act_cnt, warn_code, n_fields, act_type, warning)
|
452
|
+
@statement_no = stmt_no
|
453
|
+
@activity_count = act_cnt
|
454
|
+
@warning_code = warn_code
|
455
|
+
@num_fields = n_fields
|
456
|
+
@activity_type = act_type
|
457
|
+
@warning = warning
|
458
|
+
end
|
459
|
+
|
460
|
+
attr_reader :statement_no
|
461
|
+
attr_reader :activity_count
|
462
|
+
attr_reader :acitivity_type
|
463
|
+
attr_reader :n_fields
|
464
|
+
attr_reader :warning_code
|
465
|
+
attr_reader :warning
|
466
|
+
|
467
|
+
def inspect
|
468
|
+
"\#<Success \##{@statement_no} cnt=#{@activity_count}>"
|
469
|
+
end
|
470
|
+
|
471
|
+
def error_code
|
472
|
+
0
|
473
|
+
end
|
474
|
+
|
475
|
+
def info
|
476
|
+
nil
|
477
|
+
end
|
478
|
+
|
479
|
+
def message
|
480
|
+
''
|
481
|
+
end
|
482
|
+
|
483
|
+
def succeeded?
|
484
|
+
true
|
485
|
+
end
|
486
|
+
|
487
|
+
def failure?
|
488
|
+
false
|
489
|
+
end
|
490
|
+
|
491
|
+
def error?
|
492
|
+
false
|
493
|
+
end
|
494
|
+
|
495
|
+
def value
|
496
|
+
end
|
497
|
+
|
498
|
+
def warned?
|
499
|
+
@warning_code != 0
|
500
|
+
end
|
501
|
+
|
502
|
+
ACTIVITY_ECHO = 33
|
503
|
+
|
504
|
+
def echo?
|
505
|
+
@activity_type == ACTIVITY_ECHO
|
506
|
+
end
|
507
|
+
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
class FailureStatus
|
512
|
+
|
513
|
+
def FailureStatus.parse(parcel_data)
|
514
|
+
stmt_no, info, code, msg_len = parcel_data.unpack('SSSS')
|
515
|
+
new(stmt_no, code, info, parcel_data[8, msg_len])
|
516
|
+
end
|
517
|
+
|
518
|
+
def initialize(stmt_no, error_code, info, msg)
|
519
|
+
@statement_no = stmt_no
|
520
|
+
@error_code = error_code
|
521
|
+
@info = info
|
522
|
+
@message = msg
|
523
|
+
end
|
524
|
+
|
525
|
+
attr_reader :statement_no
|
526
|
+
attr_reader :error_code
|
527
|
+
attr_reader :info # error_code dependent additional (error) information.
|
528
|
+
attr_reader :message
|
529
|
+
|
530
|
+
def inspect
|
531
|
+
"\#<Failure \##{@statement_no} [#{@error_code}] #{@message}>"
|
532
|
+
end
|
533
|
+
|
534
|
+
def activity_count
|
535
|
+
nil
|
536
|
+
end
|
537
|
+
|
538
|
+
def warning_code
|
539
|
+
nil
|
540
|
+
end
|
541
|
+
|
542
|
+
def n_fields
|
543
|
+
nil
|
544
|
+
end
|
545
|
+
|
546
|
+
def warning
|
547
|
+
nil
|
548
|
+
end
|
549
|
+
|
550
|
+
def succeeded?
|
551
|
+
false
|
552
|
+
end
|
553
|
+
|
554
|
+
def failure?
|
555
|
+
false
|
556
|
+
end
|
557
|
+
|
558
|
+
def error?
|
559
|
+
false
|
560
|
+
end
|
561
|
+
|
562
|
+
ERROR_CODE_ABORT = 3514
|
563
|
+
|
564
|
+
def value
|
565
|
+
if @error_code == ERROR_CODE_ABORT
|
566
|
+
raise UserAbort.new(@error_code, @info, @message)
|
567
|
+
else
|
568
|
+
raise SQLError.new(@error_code, @info,
|
569
|
+
"SQL error [#{@error_code}]: #{@message}")
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def warned?
|
574
|
+
false
|
575
|
+
end
|
576
|
+
|
577
|
+
def echo?
|
578
|
+
false
|
579
|
+
end
|
580
|
+
|
581
|
+
end
|
582
|
+
|
583
|
+
|
584
|
+
# PclERROR means CLI or MTDP error.
|
585
|
+
# PclFAILURE and PclERROR have same data format, we reuse its code.
|
586
|
+
class ErrorStatus < FailureStatus
|
587
|
+
|
588
|
+
def inspect
|
589
|
+
"\#<Error \##{@statement_no} [#{@error_code}] #{@message}>"
|
590
|
+
end
|
591
|
+
|
592
|
+
def failure?
|
593
|
+
false
|
594
|
+
end
|
595
|
+
|
596
|
+
def error?
|
597
|
+
true
|
598
|
+
end
|
599
|
+
|
600
|
+
def value
|
601
|
+
raise Error, "CLI error: #{@message}"
|
602
|
+
end
|
603
|
+
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
class ResultSet
|
608
|
+
|
609
|
+
include Enumerable
|
610
|
+
extend Forwardable
|
611
|
+
|
612
|
+
def initialize(status, cli)
|
613
|
+
@status = status
|
614
|
+
@cli = cli
|
615
|
+
@next = nil
|
616
|
+
@closed = false
|
617
|
+
@metadata_read = false
|
618
|
+
@metadata = nil
|
619
|
+
@valued = false
|
620
|
+
@entries = nil
|
621
|
+
end
|
622
|
+
|
623
|
+
def inspect
|
624
|
+
"\#<ResultSet #{@status.inspect} next=#{@next.inspect}>"
|
625
|
+
end
|
626
|
+
|
627
|
+
def_delegator '@status', :error_code
|
628
|
+
def_delegator '@status', :info
|
629
|
+
def_delegator '@status', :message
|
630
|
+
def_delegator '@status', :statement_no
|
631
|
+
def_delegator '@status', :activity_count
|
632
|
+
def_delegator '@status', :n_fields
|
633
|
+
def_delegator '@status', :warning_code
|
634
|
+
def_delegator '@status', :warning
|
635
|
+
|
636
|
+
def next
|
637
|
+
return @next if @next
|
638
|
+
close unless closed?
|
639
|
+
value
|
640
|
+
rs = @cli.read_result_set
|
641
|
+
@next = rs
|
642
|
+
rs.value if rs
|
643
|
+
rs
|
644
|
+
end
|
645
|
+
|
646
|
+
def each_result_set
|
647
|
+
rs = self
|
648
|
+
while rs
|
649
|
+
begin
|
650
|
+
yield rs
|
651
|
+
ensure
|
652
|
+
rs.close unless rs.closed?
|
653
|
+
end
|
654
|
+
rs = rs.next
|
655
|
+
end
|
656
|
+
nil
|
657
|
+
end
|
658
|
+
|
659
|
+
def value_all
|
660
|
+
each_result_set do |rs|
|
661
|
+
;
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
def value
|
666
|
+
return if @valued
|
667
|
+
@status.value
|
668
|
+
@valued = true
|
669
|
+
end
|
670
|
+
|
671
|
+
def closed?
|
672
|
+
@closed
|
673
|
+
end
|
674
|
+
|
675
|
+
def close
|
676
|
+
check_connection
|
677
|
+
@cli.skip_current_statement
|
678
|
+
@closed = true
|
679
|
+
end
|
680
|
+
|
681
|
+
def each_record(&block)
|
682
|
+
return @entries.each(&block) if @entries
|
683
|
+
check_connection
|
684
|
+
unless @metadata_read
|
685
|
+
@metadata = @cli.read_metadata
|
686
|
+
unless @metadata
|
687
|
+
@closed = true
|
688
|
+
return
|
689
|
+
end
|
690
|
+
@metadata_read = true
|
691
|
+
end
|
692
|
+
while rec = @cli.read_record
|
693
|
+
yield @metadata.unmarshal(rec)
|
694
|
+
end
|
695
|
+
@closed = true
|
696
|
+
end
|
697
|
+
|
698
|
+
alias each each_record
|
699
|
+
|
700
|
+
# read all record and return it
|
701
|
+
def entries
|
702
|
+
return @entries if @entries
|
703
|
+
check_connection
|
704
|
+
map {|rec| rec }
|
705
|
+
end
|
706
|
+
|
707
|
+
# read all records and save it for later reference.
|
708
|
+
def fetch_all
|
709
|
+
return if @entries
|
710
|
+
check_connection
|
711
|
+
@entries = map {|rec| rec }
|
712
|
+
nil
|
713
|
+
end
|
714
|
+
|
715
|
+
private
|
716
|
+
|
717
|
+
def check_connection
|
718
|
+
raise ConnectionError, "already closed ResultSet" if closed?
|
719
|
+
end
|
720
|
+
|
721
|
+
end
|
722
|
+
|
723
|
+
|
724
|
+
class MetaData
|
725
|
+
|
726
|
+
def MetaData.parse_prepinfo(binary, extractor)
|
727
|
+
f = StringIO.new(binary)
|
728
|
+
cost_estimate, summary_count = f.read(10).unpack('dS')
|
729
|
+
return new([]) if f.eof? # does not have column count
|
730
|
+
count, = f.read(2).unpack('S')
|
731
|
+
new(count.times.map {
|
732
|
+
type, data_len, name_len = f.read(6).unpack('SSS')
|
733
|
+
column_name = f.read(name_len)
|
734
|
+
format_len, = f.read(2).unpack('S')
|
735
|
+
format = f.read(format_len)
|
736
|
+
title_len, = f.read(2).unpack('S')
|
737
|
+
title = f.read(title_len)
|
738
|
+
FieldType.create(type, data_len, column_name, format, title, extractor)
|
739
|
+
})
|
740
|
+
end
|
741
|
+
|
742
|
+
def MetaData.parse_datainfo(binary)
|
743
|
+
n_entries, *entries = binary.unpack('S*')
|
744
|
+
unless entries.size % 2 == 0 and entries.size / 2 == n_entries
|
745
|
+
raise MetaDataFormatError, "could not get correct size of metadata (expected=#{n_entries * 2}, really=#{entries.size})"
|
746
|
+
end
|
747
|
+
new(entries.each_slice(2).map {|type, len| FieldType.create(type, len) })
|
748
|
+
end
|
749
|
+
|
750
|
+
def initialize(types)
|
751
|
+
@types = types
|
752
|
+
end
|
753
|
+
|
754
|
+
def num_columns
|
755
|
+
@types.size
|
756
|
+
end
|
757
|
+
|
758
|
+
def column(nth)
|
759
|
+
@types[nth]
|
760
|
+
end
|
761
|
+
|
762
|
+
def each_column(&block)
|
763
|
+
@types.each(&block)
|
764
|
+
end
|
765
|
+
|
766
|
+
def field_names
|
767
|
+
@types.map {|t| t.name }
|
768
|
+
end
|
769
|
+
|
770
|
+
def inspect
|
771
|
+
"\#<#{self.class} [#{@types.map {|t| t.to_s }.join(', ')}]>"
|
772
|
+
end
|
773
|
+
|
774
|
+
def unmarshal(data)
|
775
|
+
f = StringIO.new(data)
|
776
|
+
cols = @types.zip(read_indicator(f)).map {|type, is_null|
|
777
|
+
val = type.unmarshal(f) # We must read value regardless of NULL.
|
778
|
+
is_null ? nil : val
|
779
|
+
}
|
780
|
+
Record.new(self, @types.zip(cols).map {|type, col| Field.new(type, col) })
|
781
|
+
end
|
782
|
+
|
783
|
+
private
|
784
|
+
|
785
|
+
def read_indicator(f)
|
786
|
+
f.read(num_indicator_bytes())\
|
787
|
+
.unpack(indicator_template()).first\
|
788
|
+
.split(//)[0, num_indicator_bits()]\
|
789
|
+
.map {|c| c == '1' }
|
790
|
+
end
|
791
|
+
|
792
|
+
def indicator_template
|
793
|
+
'B' + (num_indicator_bytes() * 8).to_s
|
794
|
+
end
|
795
|
+
|
796
|
+
def num_indicator_bytes
|
797
|
+
(num_indicator_bits() + 7) / 8
|
798
|
+
end
|
799
|
+
|
800
|
+
def num_indicator_bits
|
801
|
+
@types.size
|
802
|
+
end
|
803
|
+
|
804
|
+
end
|
805
|
+
|
806
|
+
|
807
|
+
# Unsupported Types:
|
808
|
+
# BLOB 400
|
809
|
+
# BLOB_DEFERRED 404
|
810
|
+
# BLOB_LOCATOR 408
|
811
|
+
# CLOB 416
|
812
|
+
# CLOB_DEFERRED 420
|
813
|
+
# CLOB_LOCATOR 424
|
814
|
+
# GRAPHIC_NN 468
|
815
|
+
# GRAPHIC_N 469
|
816
|
+
# LONG_VARBYTE_NN 696
|
817
|
+
# LONG_VARBYTE_N 697
|
818
|
+
# LONG_VARCHAR_NN 456
|
819
|
+
# LONG_VARCHAR_N 457
|
820
|
+
# LONG_VARGRAPHIC_NN 472
|
821
|
+
# LONG_VARGRAPHIC_N 473
|
822
|
+
# VARGRAPHIC_NN 464
|
823
|
+
# VARGRAPHIC_N 465
|
824
|
+
|
825
|
+
class FieldType
|
826
|
+
@@types = {}
|
827
|
+
|
828
|
+
def self.bind_code(name, code)
|
829
|
+
@@types[code] = [name, self]
|
830
|
+
end
|
831
|
+
private_class_method :bind_code
|
832
|
+
|
833
|
+
def FieldType.create(code, len, name, format, title, extractor)
|
834
|
+
type_name, type_class = @@types[code]
|
835
|
+
raise MetaDataFormatError, "unknown type code: #{code}" unless name
|
836
|
+
type_class.new(type_name, code, len, name, format, title, extractor)
|
837
|
+
end
|
838
|
+
|
839
|
+
def FieldType.codes
|
840
|
+
@@types.keys
|
841
|
+
end
|
842
|
+
|
843
|
+
def FieldType.code_names
|
844
|
+
@@types.values.map {|name, c| name }
|
845
|
+
end
|
846
|
+
|
847
|
+
def initialize(type_name, type_code, len, name, format, title, extractor)
|
848
|
+
@type_name = type_name
|
849
|
+
@type_code = type_code
|
850
|
+
@length = len
|
851
|
+
@name = name
|
852
|
+
@format = format
|
853
|
+
@title = title
|
854
|
+
@extractor = extractor
|
855
|
+
end
|
856
|
+
|
857
|
+
attr_reader :type_name
|
858
|
+
attr_reader :type_code
|
859
|
+
attr_reader :name
|
860
|
+
attr_reader :format
|
861
|
+
attr_reader :title
|
862
|
+
|
863
|
+
def to_s
|
864
|
+
"(#{@name} #{@type_name}:#{@type_code})"
|
865
|
+
end
|
866
|
+
|
867
|
+
def inspect
|
868
|
+
"\#<FieldType #{@name} (#{@type_name}:#{@type_code})>"
|
869
|
+
end
|
870
|
+
|
871
|
+
# default implementation: only read as string.
|
872
|
+
def unmarshal(f)
|
873
|
+
f.read(@length)
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
# CHAR: fixed-length character string
|
878
|
+
# BYTE: fixed-length byte string
|
879
|
+
class FixStringType < FieldType
|
880
|
+
bind_code :CHAR_NN, 452
|
881
|
+
bind_code :CHAR_N, 453
|
882
|
+
bind_code :BYTE_NN, 692
|
883
|
+
bind_code :BYTE_N, 693
|
884
|
+
|
885
|
+
def unmarshal(f)
|
886
|
+
@extractor.extract(f.read(@length))
|
887
|
+
end
|
888
|
+
end
|
889
|
+
|
890
|
+
# VARCHAR: variable-length character string
|
891
|
+
# VARBYTE: variable-length byte string
|
892
|
+
class VarStringType < FieldType
|
893
|
+
bind_code :VARCHAR_NN, 448
|
894
|
+
bind_code :VARCHAR_N, 449
|
895
|
+
bind_code :VARBYTE_NN, 688
|
896
|
+
bind_code :VARBYTE_N, 689
|
897
|
+
|
898
|
+
def unmarshal(f)
|
899
|
+
real_len = f.read(2).unpack('S').first
|
900
|
+
@extractor.extract(f.read(real_len))
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
# 1 byte signed integer
|
905
|
+
class ByteIntType < FieldType
|
906
|
+
bind_code :BYTEINT_NN, 756
|
907
|
+
bind_code :BYTEINT_N, 757
|
908
|
+
|
909
|
+
def unmarshal(f)
|
910
|
+
f.read(@length).unpack('c').first
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
# 2 byte signed integer
|
915
|
+
class SmallIntType < FieldType
|
916
|
+
bind_code :SMALLINT_NN, 500
|
917
|
+
bind_code :SMALLINT_N, 501
|
918
|
+
|
919
|
+
def unmarshal(f)
|
920
|
+
f.read(@length).unpack('s').first
|
921
|
+
end
|
922
|
+
end
|
923
|
+
|
924
|
+
# 4 byte signed integer
|
925
|
+
class IntegerType < FieldType
|
926
|
+
bind_code :INTEGER_NN, 496
|
927
|
+
bind_code :INTEGER_N, 497
|
928
|
+
|
929
|
+
def unmarshal(f)
|
930
|
+
f.read(@length).unpack('l').first
|
931
|
+
end
|
932
|
+
end
|
933
|
+
|
934
|
+
# 8 byte signed integer
|
935
|
+
class BigIntType < FieldType
|
936
|
+
bind_code :BIGINT_NN, 600
|
937
|
+
bind_code :BIGINT_N, 601
|
938
|
+
|
939
|
+
def unmarshal(f)
|
940
|
+
f.read(@length).unpack('q').first
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
class FloatType < FieldType
|
945
|
+
bind_code :FLOAT_NN, 480
|
946
|
+
bind_code :FLOAT_N, 481
|
947
|
+
|
948
|
+
def unmarshal(f)
|
949
|
+
f.read(@length).unpack('d').first
|
950
|
+
end
|
951
|
+
end
|
952
|
+
|
953
|
+
class DecimalType < FieldType
|
954
|
+
bind_code :DECIMAL_NN, 484
|
955
|
+
bind_code :DECIMAL_N, 485
|
956
|
+
|
957
|
+
def initialize(type_name, type_code, len, name, format, title, extractor)
|
958
|
+
super
|
959
|
+
@width, @fractional = len.divmod(256)
|
960
|
+
@length, @template = get_binary_data(@width)
|
961
|
+
end
|
962
|
+
|
963
|
+
def get_binary_data(width)
|
964
|
+
case
|
965
|
+
when width <= 2 then return 1, 'c'
|
966
|
+
when width <= 4 then return 2, 's'
|
967
|
+
when width <= 9 then return 4, 'l'
|
968
|
+
when width <= 18 then return 8, 'q'
|
969
|
+
else return 16, nil
|
970
|
+
end
|
971
|
+
end
|
972
|
+
|
973
|
+
attr_reader :width
|
974
|
+
attr_reader :fractional
|
975
|
+
|
976
|
+
def unmarshal(f)
|
977
|
+
insert_fp(read_base_int(f).to_s, @fractional)
|
978
|
+
end
|
979
|
+
|
980
|
+
private
|
981
|
+
|
982
|
+
def read_base_int(f)
|
983
|
+
if @template
|
984
|
+
f.read(@length).unpack(@template).first
|
985
|
+
else
|
986
|
+
# PLATFORM SPECIFIC: little endian
|
987
|
+
lower, upper = f.read(@length).unpack('qQ')
|
988
|
+
sign = upper >= 0 ? +1 : -1
|
989
|
+
sign * (upper.abs << 64 | lower)
|
990
|
+
end
|
991
|
+
end
|
992
|
+
|
993
|
+
def insert_fp(str, frac)
|
994
|
+
if frac == 0
|
995
|
+
str
|
996
|
+
else
|
997
|
+
return '0.' + str if str.size == frac
|
998
|
+
str[-frac, 0] = '.'
|
999
|
+
str
|
1000
|
+
end
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
class DateType < FieldType
|
1005
|
+
bind_code :DATE_NN, 752
|
1006
|
+
bind_code :DATE_N, 753
|
1007
|
+
|
1008
|
+
def unmarshal(f)
|
1009
|
+
d = (f.read(@length).unpack('l').first + 19000000).to_s
|
1010
|
+
d[0,4] + '-' + d[4,2] + '-' + d[6,2]
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
# TIME, TIMESTAMP are same as CHAR.
|
1015
|
+
|
1016
|
+
|
1017
|
+
class Record
|
1018
|
+
|
1019
|
+
include Enumerable
|
1020
|
+
|
1021
|
+
def initialize(metadata, fields)
|
1022
|
+
@metadata = metadata
|
1023
|
+
@fields = fields
|
1024
|
+
@index = build_name_index(metadata)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
def build_name_index(meta)
|
1028
|
+
h = {}
|
1029
|
+
idx = 0
|
1030
|
+
meta.each_column do |c|
|
1031
|
+
h[c.name.downcase] = idx
|
1032
|
+
h[idx] = idx
|
1033
|
+
idx += 1
|
1034
|
+
end
|
1035
|
+
h
|
1036
|
+
end
|
1037
|
+
private :build_name_index
|
1038
|
+
|
1039
|
+
def size
|
1040
|
+
@fields.size
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
def keys
|
1044
|
+
@metadata.field_names
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
def [](key)
|
1048
|
+
field(key).value
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
def field(key)
|
1052
|
+
i = (@index[key.to_s.downcase] || @index[key]) or
|
1053
|
+
raise ArgumentError, "bad field key: #{key}"
|
1054
|
+
@fields[i]
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def each_field(&block)
|
1058
|
+
@fields.each(&block)
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def each_value
|
1062
|
+
@fields.each {|c|
|
1063
|
+
yield c.value
|
1064
|
+
}
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
alias each each_value
|
1068
|
+
|
1069
|
+
def values_at(*keys)
|
1070
|
+
keys.map {|k| self[k] }
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
def to_a
|
1074
|
+
@fields.map {|f| f.value }
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def to_h
|
1078
|
+
h = {}
|
1079
|
+
@metadata.field_names.zip(@fields) do |name, field|
|
1080
|
+
h[name] = field.value
|
1081
|
+
end
|
1082
|
+
h
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
def inspect
|
1086
|
+
"\#<Record #{@fields.map {|c| c.to_s }.join(', ')}>"
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
|
1092
|
+
class Field
|
1093
|
+
|
1094
|
+
def initialize(metadata, value)
|
1095
|
+
@metadata = metadata
|
1096
|
+
@value = value
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
attr_reader :value
|
1100
|
+
alias data value
|
1101
|
+
|
1102
|
+
extend Forwardable
|
1103
|
+
def_delegator "@metadata", :name
|
1104
|
+
def_delegator "@metadata", :format
|
1105
|
+
def_delegator "@metadata", :title
|
1106
|
+
|
1107
|
+
def type
|
1108
|
+
@metadata.type_name
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
def type_code
|
1112
|
+
@metadata.type_code
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
def null?
|
1116
|
+
@value.nil?
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def to_s
|
1120
|
+
"(#{name} #{@value.inspect})"
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
end
|