teradata-cli 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'
@@ -0,0 +1,4 @@
1
+ # cli.so requires Teradata::Error, load teradata/exception first.
2
+ require "teradata/cli/version"
3
+ require 'teradata/exception'
4
+ require 'teradata/cli.so'
@@ -0,0 +1,5 @@
1
+ module Teradata
2
+ module Cli
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -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