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.
@@ -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