pg 1.2.3 → 1.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.appveyor.yml +36 -0
  4. data/.gems +6 -0
  5. data/.github/workflows/binary-gems.yml +86 -0
  6. data/.github/workflows/source-gem.yml +129 -0
  7. data/.gitignore +13 -0
  8. data/.hgsigs +34 -0
  9. data/.hgtags +41 -0
  10. data/.irbrc +23 -0
  11. data/.pryrc +23 -0
  12. data/.tm_properties +21 -0
  13. data/.travis.yml +49 -0
  14. data/Gemfile +14 -0
  15. data/History.rdoc +153 -7
  16. data/Manifest.txt +0 -1
  17. data/README.rdoc +7 -6
  18. data/Rakefile +27 -138
  19. data/Rakefile.cross +8 -5
  20. data/certs/ged.pem +24 -0
  21. data/certs/larskanis-2022.pem +26 -0
  22. data/ext/errorcodes.def +8 -0
  23. data/ext/errorcodes.rb +0 -0
  24. data/ext/errorcodes.txt +3 -1
  25. data/ext/extconf.rb +131 -25
  26. data/ext/gvl_wrappers.c +4 -0
  27. data/ext/gvl_wrappers.h +23 -0
  28. data/ext/pg.c +59 -4
  29. data/ext/pg.h +19 -1
  30. data/ext/pg_coder.c +82 -28
  31. data/ext/pg_connection.c +680 -508
  32. data/ext/pg_copy_coder.c +45 -16
  33. data/ext/pg_record_coder.c +45 -15
  34. data/ext/pg_result.c +77 -40
  35. data/ext/pg_text_decoder.c +1 -1
  36. data/ext/pg_text_encoder.c +6 -6
  37. data/ext/pg_tuple.c +49 -29
  38. data/ext/pg_type_map.c +41 -8
  39. data/ext/pg_type_map_all_strings.c +15 -1
  40. data/ext/pg_type_map_by_class.c +49 -24
  41. data/ext/pg_type_map_by_column.c +66 -28
  42. data/ext/pg_type_map_by_mri_type.c +47 -18
  43. data/ext/pg_type_map_by_oid.c +52 -23
  44. data/ext/pg_type_map_in_ruby.c +50 -19
  45. data/ext/pg_util.c +2 -2
  46. data/lib/pg/basic_type_map_based_on_result.rb +47 -0
  47. data/lib/pg/basic_type_map_for_queries.rb +193 -0
  48. data/lib/pg/basic_type_map_for_results.rb +81 -0
  49. data/lib/pg/basic_type_registry.rb +301 -0
  50. data/lib/pg/coder.rb +1 -1
  51. data/lib/pg/connection.rb +589 -58
  52. data/lib/pg/version.rb +4 -0
  53. data/lib/pg.rb +47 -32
  54. data/misc/openssl-pg-segfault.rb +31 -0
  55. data/misc/postgres/History.txt +9 -0
  56. data/misc/postgres/Manifest.txt +5 -0
  57. data/misc/postgres/README.txt +21 -0
  58. data/misc/postgres/Rakefile +21 -0
  59. data/misc/postgres/lib/postgres.rb +16 -0
  60. data/misc/ruby-pg/History.txt +9 -0
  61. data/misc/ruby-pg/Manifest.txt +5 -0
  62. data/misc/ruby-pg/README.txt +21 -0
  63. data/misc/ruby-pg/Rakefile +21 -0
  64. data/misc/ruby-pg/lib/ruby/pg.rb +16 -0
  65. data/pg.gemspec +32 -0
  66. data/rakelib/task_extension.rb +46 -0
  67. data/sample/array_insert.rb +20 -0
  68. data/sample/async_api.rb +102 -0
  69. data/sample/async_copyto.rb +39 -0
  70. data/sample/async_mixed.rb +56 -0
  71. data/sample/check_conn.rb +21 -0
  72. data/sample/copydata.rb +71 -0
  73. data/sample/copyfrom.rb +81 -0
  74. data/sample/copyto.rb +19 -0
  75. data/sample/cursor.rb +21 -0
  76. data/sample/disk_usage_report.rb +177 -0
  77. data/sample/issue-119.rb +94 -0
  78. data/sample/losample.rb +69 -0
  79. data/sample/minimal-testcase.rb +17 -0
  80. data/sample/notify_wait.rb +72 -0
  81. data/sample/pg_statistics.rb +285 -0
  82. data/sample/replication_monitor.rb +222 -0
  83. data/sample/test_binary_values.rb +33 -0
  84. data/sample/wal_shipper.rb +434 -0
  85. data/sample/warehouse_partitions.rb +311 -0
  86. data.tar.gz.sig +0 -0
  87. metadata +87 -224
  88. metadata.gz.sig +0 -0
  89. data/ChangeLog +0 -0
  90. data/lib/pg/basic_type_mapping.rb +0 -522
  91. data/spec/data/expected_trace.out +0 -26
  92. data/spec/data/random_binary_data +0 -0
  93. data/spec/helpers.rb +0 -380
  94. data/spec/pg/basic_type_mapping_spec.rb +0 -630
  95. data/spec/pg/connection_spec.rb +0 -1949
  96. data/spec/pg/connection_sync_spec.rb +0 -41
  97. data/spec/pg/result_spec.rb +0 -681
  98. data/spec/pg/tuple_spec.rb +0 -333
  99. data/spec/pg/type_map_by_class_spec.rb +0 -138
  100. data/spec/pg/type_map_by_column_spec.rb +0 -226
  101. data/spec/pg/type_map_by_mri_type_spec.rb +0 -136
  102. data/spec/pg/type_map_by_oid_spec.rb +0 -149
  103. data/spec/pg/type_map_in_ruby_spec.rb +0 -164
  104. data/spec/pg/type_map_spec.rb +0 -22
  105. data/spec/pg/type_spec.rb +0 -1123
  106. data/spec/pg_spec.rb +0 -50
@@ -0,0 +1,285 @@
1
+ # -*- ruby -*-
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+ #
4
+ # PostgreSQL statistic gatherer.
5
+ # Mahlon E. Smith <mahlon@martini.nu>
6
+ #
7
+ # Based on queries by Kenny Gorman.
8
+ # http://www.kennygorman.com/wordpress/?page_id=491
9
+ #
10
+ # An example gnuplot input script is included in the __END__ block
11
+ # of this script. Using it, you can feed the output this script
12
+ # generates to gnuplot (after removing header lines) to generate
13
+ # some nice performance charts.
14
+ #
15
+
16
+ require 'ostruct'
17
+ require 'optparse'
18
+ require 'etc'
19
+ require 'pg'
20
+
21
+
22
+ ### PostgreSQL Stats. Fetch information from pg_stat_* tables.
23
+ ### Optionally run in a continuous loop, displaying deltas.
24
+ ###
25
+ class Stats
26
+ VERSION = %q$Id$
27
+
28
+ def initialize( opts )
29
+ @opts = opts
30
+ @db = PG.connect(
31
+ :dbname => opts.database,
32
+ :host => opts.host,
33
+ :port => opts.port,
34
+ :user => opts.user,
35
+ :password => opts.pass,
36
+ :sslmode => 'prefer'
37
+ )
38
+ @last = nil
39
+ end
40
+
41
+ ######
42
+ public
43
+ ######
44
+
45
+ ### Primary loop. Gather statistics and generate deltas.
46
+ ###
47
+ def run
48
+ run_count = 0
49
+
50
+ loop do
51
+ current_stat = self.get_stats
52
+
53
+ # First run, store and continue
54
+ #
55
+ if @last.nil?
56
+ @last = current_stat
57
+ sleep @opts.interval
58
+ next
59
+ end
60
+
61
+ # headers
62
+ #
63
+ if run_count == 0 || run_count % 50 == 0
64
+ puts "%-20s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s" % %w[
65
+ time commits rollbks blksrd blkshit bkends seqscan
66
+ seqtprd idxscn idxtrd ins upd del locks activeq
67
+ ]
68
+ end
69
+
70
+ # calculate deltas
71
+ #
72
+ delta = current_stat.inject({}) do |h, pair|
73
+ stat, val = *pair
74
+
75
+ if %w[ activeq locks bkends ].include?( stat )
76
+ h[stat] = current_stat[stat].to_i
77
+ else
78
+ h[stat] = current_stat[stat].to_i - @last[stat].to_i
79
+ end
80
+
81
+ h
82
+ end
83
+ delta[ 'time' ] = Time.now.strftime('%F %T')
84
+
85
+ # new values
86
+ #
87
+ puts "%-20s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s" % [
88
+ delta['time'], delta['commits'], delta['rollbks'], delta['blksrd'],
89
+ delta['blkshit'], delta['bkends'], delta['seqscan'],
90
+ delta['seqtprd'], delta['idxscn'], delta['idxtrd'],
91
+ delta['ins'], delta['upd'], delta['del'], delta['locks'], delta['activeq']
92
+ ]
93
+
94
+ @last = current_stat
95
+ run_count += 1
96
+ sleep @opts.interval
97
+ end
98
+ end
99
+
100
+
101
+ ### Query the database for performance measurements. Returns a hash.
102
+ ###
103
+ def get_stats
104
+ res = @db.exec %Q{
105
+ SELECT
106
+ MAX(stat_db.xact_commit) AS commits,
107
+ MAX(stat_db.xact_rollback) AS rollbks,
108
+ MAX(stat_db.blks_read) AS blksrd,
109
+ MAX(stat_db.blks_hit) AS blkshit,
110
+ MAX(stat_db.numbackends) AS bkends,
111
+ SUM(stat_tables.seq_scan) AS seqscan,
112
+ SUM(stat_tables.seq_tup_read) AS seqtprd,
113
+ SUM(stat_tables.idx_scan) AS idxscn,
114
+ SUM(stat_tables.idx_tup_fetch) AS idxtrd,
115
+ SUM(stat_tables.n_tup_ins) AS ins,
116
+ SUM(stat_tables.n_tup_upd) AS upd,
117
+ SUM(stat_tables.n_tup_del) AS del,
118
+ MAX(stat_locks.locks) AS locks,
119
+ MAX(activity.sess) AS activeq
120
+ FROM
121
+ pg_stat_database AS stat_db,
122
+ pg_stat_user_tables AS stat_tables,
123
+ (SELECT COUNT(*) AS locks FROM pg_locks ) AS stat_locks,
124
+ (SELECT COUNT(*) AS sess FROM pg_stat_activity WHERE current_query <> '<IDLE>') AS activity
125
+ WHERE
126
+ stat_db.datname = '%s';
127
+ } % [ @opts.database ]
128
+
129
+ return res[0]
130
+ end
131
+ end
132
+
133
+
134
+ ### Parse command line arguments. Return a struct of global options.
135
+ ###
136
+ def parse_args( args )
137
+ options = OpenStruct.new
138
+ options.database = Etc.getpwuid( Process.uid ).name
139
+ options.host = '127.0.0.1'
140
+ options.port = 5432
141
+ options.user = Etc.getpwuid( Process.uid ).name
142
+ options.sslmode = 'disable'
143
+ options.interval = 5
144
+
145
+ opts = OptionParser.new do |opts|
146
+ opts.banner = "Usage: #{$0} [options]"
147
+
148
+ opts.separator ''
149
+ opts.separator 'Connection options:'
150
+
151
+ opts.on( '-d', '--database DBNAME',
152
+ "specify the database to connect to (default: \"#{options.database}\")" ) do |db|
153
+ options.database = db
154
+ end
155
+
156
+ opts.on( '-h', '--host HOSTNAME', 'database server host' ) do |host|
157
+ options.host = host
158
+ end
159
+
160
+ opts.on( '-p', '--port PORT', Integer,
161
+ "database server port (default: \"#{options.port}\")" ) do |port|
162
+ options.port = port
163
+ end
164
+
165
+ opts.on( '-U', '--user NAME',
166
+ "database user name (default: \"#{options.user}\")" ) do |user|
167
+ options.user = user
168
+ end
169
+
170
+ opts.on( '-W', 'force password prompt' ) do |pw|
171
+ print 'Password: '
172
+ begin
173
+ system 'stty -echo'
174
+ options.pass = gets.chomp
175
+ ensure
176
+ system 'stty echo'
177
+ puts
178
+ end
179
+ end
180
+
181
+ opts.separator ''
182
+ opts.separator 'Other options:'
183
+
184
+ opts.on( '-i', '--interval SECONDS', Integer,
185
+ "refresh interval in seconds (default: \"#{options.interval}\")") do |seconds|
186
+ options.interval = seconds
187
+ end
188
+
189
+ opts.on_tail( '--help', 'show this help, then exit' ) do
190
+ $stderr.puts opts
191
+ exit
192
+ end
193
+
194
+ opts.on_tail( '--version', 'output version information, then exit' ) do
195
+ puts Stats::VERSION
196
+ exit
197
+ end
198
+ end
199
+
200
+ opts.parse!( args )
201
+ return options
202
+ end
203
+
204
+
205
+ ### Go!
206
+ ###
207
+ if __FILE__ == $0
208
+ $stdout.sync = true
209
+ Stats.new( parse_args( ARGV ) ).run
210
+ end
211
+
212
+
213
+ __END__
214
+ ######################################################################
215
+ ### T E R M I N A L O P T I O N S
216
+ ######################################################################
217
+
218
+ #set terminal png nocrop enhanced font arial 8 size '800x600' x000000 xffffff x444444
219
+ #set output 'graph.png'
220
+
221
+ set terminal pdf linewidth 4 size 11,8
222
+ set output 'graph.pdf'
223
+
224
+ #set terminal aqua
225
+
226
+
227
+ ######################################################################
228
+ ### O P T I O N S F O R A L L G R A P H S
229
+ ######################################################################
230
+
231
+ set multiplot layout 2,1 title "PostgreSQL Statistics\n5 second sample rate (smoothed)"
232
+
233
+ set grid x y
234
+ set key right vertical outside
235
+ set key nobox
236
+ set xdata time
237
+ set timefmt "%Y-%m-%d.%H:%M:%S"
238
+ set format x "%l%p"
239
+ set xtic rotate by -45
240
+ input_file = "database_stats.txt"
241
+
242
+ # edit to taste!
243
+ set xrange ["2012-04-16.00:00:00":"2012-04-17.00:00:00"]
244
+
245
+
246
+ ######################################################################
247
+ ### G R A P H 1
248
+ ######################################################################
249
+
250
+ set title "Database Operations and Connection Totals"
251
+ set yrange [0:200]
252
+
253
+ plot \
254
+ input_file using 1:2 title "Commits" with lines smooth bezier, \
255
+ input_file using 1:3 title "Rollbacks" with lines smooth bezier, \
256
+ input_file using 1:11 title "Inserts" with lines smooth bezier, \
257
+ input_file using 1:12 title "Updates" with lines smooth bezier, \
258
+ input_file using 1:13 title "Deletes" with lines smooth bezier, \
259
+ input_file using 1:6 title "Backends (total)" with lines, \
260
+ input_file using 1:15 title "Active queries (total)" with lines smooth bezier
261
+
262
+
263
+ ######################################################################
264
+ ### G R A P H 2
265
+ ######################################################################
266
+
267
+ set title "Backend Performance"
268
+ set yrange [0:10000]
269
+
270
+ plot \
271
+ input_file using 1:4 title "Block (cache) reads" with lines smooth bezier, \
272
+ input_file using 1:5 title "Block (cache) hits" with lines smooth bezier, \
273
+ input_file using 1:7 title "Sequence scans" with lines smooth bezier, \
274
+ input_file using 1:8 title "Sequence tuple reads" with lines smooth bezier, \
275
+ input_file using 1:9 title "Index scans" with lines smooth bezier, \
276
+ input_file using 1:10 title "Index tuple reads" with lines smooth bezier
277
+
278
+
279
+ ######################################################################
280
+ ### C L E A N U P
281
+ ######################################################################
282
+
283
+ unset multiplot
284
+ reset
285
+
@@ -0,0 +1,222 @@
1
+ # -*- ruby -*-
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+ #
4
+ # Get the current WAL segment and offset from a master postgresql
5
+ # server, and compare slave servers to see how far behind they
6
+ # are in MB. This script should be easily modified for use with
7
+ # Nagios/Mon/Monit/Zabbix/whatever, or wrapping it in a display loop,
8
+ # and is suitable for both WAL shipping or streaming forms of replication.
9
+ #
10
+ # Mahlon E. Smith <mahlon@martini.nu>
11
+ #
12
+ # First argument is the master server, all other arguments are treated
13
+ # as slave machines.
14
+ #
15
+ # db_replication.monitor db-master.example.com ...
16
+ #
17
+
18
+ require 'ostruct'
19
+ require 'optparse'
20
+ require 'pathname'
21
+ require 'etc'
22
+ require 'pg'
23
+ require 'pp'
24
+
25
+
26
+ ### A class to encapsulate the PG handles.
27
+ ###
28
+ class PGMonitor
29
+
30
+ VERSION = %q$Id$
31
+
32
+ # When to consider a slave as 'behind', measured in WAL segments.
33
+ # The default WAL segment size is 16, so we'll alert after
34
+ # missing two WAL files worth of data.
35
+ #
36
+ LAG_ALERT = 32
37
+
38
+ ### Create a new PGMonitor object.
39
+ ###
40
+ def initialize( opts, hosts )
41
+ @opts = opts
42
+ @master = hosts.shift
43
+ @slaves = hosts
44
+ @current_wal = {}
45
+ @failures = []
46
+ end
47
+
48
+ attr_reader :opts, :current_wal, :master, :slaves, :failures
49
+
50
+
51
+ ### Perform the connections and check the lag.
52
+ ###
53
+ def check
54
+ # clear prior failures, get current xlog info
55
+ @failures = []
56
+ return unless self.get_current_wal
57
+
58
+ # check all slaves
59
+ self.slaves.each do |slave|
60
+ begin
61
+ slave_db = PG.connect(
62
+ :dbname => self.opts.database,
63
+ :host => slave,
64
+ :port => self.opts.port,
65
+ :user => self.opts.user,
66
+ :password => self.opts.pass,
67
+ :sslmode => 'prefer'
68
+ )
69
+
70
+ xlog = slave_db.exec( 'SELECT pg_last_xlog_receive_location()' ).getvalue( 0, 0 )
71
+ slave_db.close
72
+
73
+ lag_in_megs = ( self.find_lag( xlog ).to_f / 1024 / 1024 ).abs
74
+ if lag_in_megs >= LAG_ALERT
75
+ failures << { :host => slave,
76
+ :error => "%0.2fMB behind the master." % [ lag_in_megs ] }
77
+ end
78
+ rescue => err
79
+ failures << { :host => slave, :error => err.message }
80
+ end
81
+ end
82
+ end
83
+
84
+
85
+ #########
86
+ protected
87
+ #########
88
+
89
+ ### Ask the master for the current xlog information, to compare
90
+ ### to slaves. Returns true on success. On failure, populates
91
+ ### the failures array and returns false.
92
+ ###
93
+ def get_current_wal
94
+ master_db = PG.connect(
95
+ :dbname => self.opts.database,
96
+ :host => self.master,
97
+ :port => self.opts.port,
98
+ :user => self.opts.user,
99
+ :password => self.opts.pass,
100
+ :sslmode => 'prefer'
101
+ )
102
+
103
+ self.current_wal[ :segbytes ] = master_db.exec( 'SHOW wal_segment_size' ).
104
+ getvalue( 0, 0 ).sub( /\D+/, '' ).to_i << 20
105
+
106
+ current = master_db.exec( 'SELECT pg_current_xlog_location()' ).getvalue( 0, 0 )
107
+ self.current_wal[ :segment ], self.current_wal[ :offset ] = current.split( /\// )
108
+
109
+ master_db.close
110
+ return true
111
+
112
+ # If we can't get any of the info from the master, then there is no
113
+ # point in a comparison with slaves.
114
+ #
115
+ rescue => err
116
+ self.failures << { :host => self.master,
117
+ :error => 'Unable to retrieve required info from the master (%s)' % [ err.message ] }
118
+
119
+ return false
120
+ end
121
+
122
+
123
+ ### Given an +xlog+ position from a slave server, return
124
+ ### the number of bytes the slave needs to replay before it
125
+ ### is caught up to the master.
126
+ ###
127
+ def find_lag( xlog )
128
+ s_segment, s_offset = xlog.split( /\// )
129
+ m_segment = self.current_wal[ :segment ]
130
+ m_offset = self.current_wal[ :offset ]
131
+ m_segbytes = self.current_wal[ :segbytes ]
132
+
133
+ return (( m_segment.hex - s_segment.hex ) * m_segbytes) + ( m_offset.hex - s_offset.hex )
134
+ end
135
+
136
+ end
137
+
138
+
139
+ ### Parse command line arguments. Return a struct of global options.
140
+ ###
141
+ def parse_args( args )
142
+ options = OpenStruct.new
143
+ options.database = 'postgres'
144
+ options.port = 5432
145
+ options.user = Etc.getpwuid( Process.uid ).name
146
+ options.sslmode = 'prefer'
147
+
148
+ opts = OptionParser.new do |opts|
149
+ opts.banner = "Usage: #{$0} [options] <master> <slave> [slave2, slave3...]"
150
+
151
+ opts.separator ''
152
+ opts.separator 'Connection options:'
153
+
154
+ opts.on( '-d', '--database DBNAME',
155
+ "specify the database to connect to (default: \"#{options.database}\")" ) do |db|
156
+ options.database = db
157
+ end
158
+
159
+ opts.on( '-h', '--host HOSTNAME', 'database server host' ) do |host|
160
+ options.host = host
161
+ end
162
+
163
+ opts.on( '-p', '--port PORT', Integer,
164
+ "database server port (default: \"#{options.port}\")" ) do |port|
165
+ options.port = port
166
+ end
167
+
168
+ opts.on( '-U', '--user NAME',
169
+ "database user name (default: \"#{options.user}\")" ) do |user|
170
+ options.user = user
171
+ end
172
+
173
+ opts.on( '-W', 'force password prompt' ) do |pw|
174
+ print 'Password: '
175
+ begin
176
+ system 'stty -echo'
177
+ options.pass = $stdin.gets.chomp
178
+ ensure
179
+ system 'stty echo'
180
+ puts
181
+ end
182
+ end
183
+
184
+ opts.separator ''
185
+ opts.separator 'Other options:'
186
+
187
+ opts.on_tail( '--help', 'show this help, then exit' ) do
188
+ $stderr.puts opts
189
+ exit
190
+ end
191
+
192
+ opts.on_tail( '--version', 'output version information, then exit' ) do
193
+ puts PGMonitor::VERSION
194
+ exit
195
+ end
196
+ end
197
+
198
+ opts.parse!( args )
199
+ return options
200
+ end
201
+
202
+
203
+
204
+ if __FILE__ == $0
205
+ opts = parse_args( ARGV )
206
+ raise ArgumentError, "At least two PostgreSQL servers are required." if ARGV.length < 2
207
+ mon = PGMonitor.new( opts, ARGV )
208
+
209
+ mon.check
210
+ if mon.failures.empty?
211
+ puts "All is well!"
212
+ exit 0
213
+ else
214
+ puts "Database replication delayed or broken."
215
+ mon.failures.each do |bad|
216
+ puts "%s: %s" % [ bad[ :host ], bad[ :error ] ]
217
+ end
218
+ exit 1
219
+ end
220
+ end
221
+
222
+
@@ -0,0 +1,33 @@
1
+ # -*- ruby -*-1.9.1
2
+
3
+ require 'pg'
4
+
5
+ db = PG.connect( :dbname => 'test' )
6
+ db.exec "DROP TABLE IF EXISTS test"
7
+ db.exec "CREATE TABLE test (a INTEGER, b BYTEA)"
8
+
9
+ a = 42
10
+ b = [1, 2, 3]
11
+ db.exec "INSERT INTO test(a, b) VALUES($1::int, $2::bytea)",
12
+ [a, {:value => b.pack('N*'), :format => 1}]
13
+
14
+ db.exec( "SELECT a::int, b::bytea FROM test LIMIT 1", [], 1 ) do |res|
15
+
16
+ res.nfields.times do |i|
17
+ puts "Field %d is: %s, a %s (%s) column from table %p" % [
18
+ i,
19
+ res.fname( i ),
20
+ db.exec( "SELECT format_type($1,$2)", [res.ftype(i), res.fmod(1)] ).getvalue(0,0),
21
+ res.fformat( i ).zero? ? "string" : "binary",
22
+ res.ftable( i ),
23
+ ]
24
+ end
25
+
26
+ res.each do |row|
27
+ puts "a = #{row['a'].inspect}"
28
+ puts "a (unpacked) = #{row['a'].unpack('N*').inspect}"
29
+ puts "b = #{row['b'].unpack('N*').inspect}"
30
+ end
31
+ end
32
+
33
+