qreport 0.0.10 → 0.1.0

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.
data/ChangeLog CHANGED
@@ -1,3 +1,14 @@
1
+ 2013-10-14 Kurt A. Stephens <ks.github@kurtstephens.com>
2
+ * v0.1.0: New Features.
3
+ * Handle Connection#close cleanly.
4
+ * Handle Rational data.
5
+ * Preliminary ARRAY[] data.
6
+ * Trap errors in ReportRun#data SELECT.
7
+
8
+ 2013-10-02 Kurt A. Stephens <ks.github@kurtstephens.com>
9
+ * v0.0.11: New Features.
10
+ * ReportRun#data.columns: avoid pulling in entire report rows.
11
+
1
12
  2013-10-01 Kurt A. Stephens <ks.github@kurtstephens.com>
2
13
  * v0.0.10: New Features.
3
14
  * Support CTE queries using "FROM (<subselect>) AS" syntax.
@@ -1,5 +1,6 @@
1
1
  require 'qreport'
2
2
  require 'time' # iso8601
3
+ require 'rational' # Rational
3
4
  require 'pp' # dump_result!
4
5
 
5
6
  module Qreport
@@ -74,17 +75,23 @@ module Qreport
74
75
 
75
76
  def close
76
77
  raise Error, "close during transaction" if in_transaction?
78
+ _close
79
+ ensure
80
+ @invalid = false
81
+ end
82
+
83
+ def _close
77
84
  if @conn
78
85
  conn = @conn
79
86
  @conn = nil
80
87
  conn.close if @conn_owned
81
88
  end
82
89
  ensure
83
- @invalid = false
84
- @transaction_nesting = 0
85
90
  @conn_owned = false
91
+ @transaction_nesting = 0
86
92
  end
87
93
 
94
+
88
95
  def in_transaction?; @transaction_nesting > 0; end
89
96
 
90
97
  def transaction
@@ -181,7 +188,8 @@ module Qreport
181
188
  conn.escape_identifier name.to_s
182
189
  end
183
190
 
184
- def escape_value val
191
+ def escape_value val, example_value = nil
192
+ example_val ||= val
185
193
  case val
186
194
  when SQL
187
195
  val.to_s
@@ -191,16 +199,30 @@ module Qreport
191
199
  T_
192
200
  when false
193
201
  F_
194
- when Integer, Float
202
+ when Rational
203
+ val.to_f
204
+ when Numeric
195
205
  val
196
206
  when String, Symbol
197
207
  "'" << conn.escape_string(val.to_s) << QUOTE
198
208
  when Time
199
- escape_value(val.iso8601(6)) << "::timestamp"
209
+ escape_value(val.iso8601(6)) << S_TIMESTAMP
200
210
  when Range
201
211
  "BETWEEN #{escape_value(val.first)} AND #{escape_value(val.last)}"
202
- when Hash, Array
212
+ when Hash
203
213
  escape_value(val.to_json)
214
+ when Array
215
+ case
216
+ when true
217
+ # PUNT!!!
218
+ escape_value(val.to_json)
219
+ # DOES NOT HANDLE EMPTY ARRAY!!!
220
+ when example_val.all?{|x| Numeric === x || x.nil?} && ! example_val.empty?
221
+ "ARRAY[#{val.map{|x| escape_value(x, example_val[0])} * ','}]"
222
+ else
223
+ # PUNT!!!
224
+ escape_value(val.to_json)
225
+ end
204
226
  else
205
227
  raise TypeError, "cannot escape_value on #{val.class.name}"
206
228
  end.to_s
@@ -210,6 +232,7 @@ module Qreport
210
232
  T_ = "'t'::boolean".freeze
211
233
  F_ = "'f'::boolean".freeze
212
234
  T = 't'.freeze
235
+ S_TIMESTAMP = "::timestamp".freeze
213
236
 
214
237
  def unescape_value val, type
215
238
  case val
@@ -227,6 +250,15 @@ module Qreport
227
250
  return func
228
251
  end
229
252
  case type
253
+ when /\[\]\Z/
254
+ et = $`
255
+ el = unescape_value_func(et)
256
+ lambda do | val, type |
257
+ # PP.pp([ val, type, et ])
258
+ val.gsub(/\A\{|\}\Z/, '').
259
+ split(',').
260
+ map{|x| x == 'NULL' ? nil : el.call(x, et)}
261
+ end
230
262
  when /^bool/
231
263
  lambda { | val, type | val == T }
232
264
  when /^(int|smallint|bigint|oid|tid|xid|cid)/
@@ -286,145 +318,28 @@ module Qreport
286
318
  end
287
319
 
288
320
  def run_query! sql, query, options = nil
321
+ error = nil
289
322
  options ||= EMPTY_Hash
323
+ # $stderr.puts " run_query! options = #{options.inspect}"
290
324
  result = nil
291
325
  begin
292
326
  result = conn.async_exec(sql)
293
327
  rescue ::PG::Error => exc
294
- # $stderr.puts " ERROR: #{exc.inspect}\n #{exc.backtrace * "\n "}"
295
- query.error = exc.inspect
296
- raise exc unless options[:capture_error]
328
+ error = exc
297
329
  rescue ::StandardError => exc
298
330
  @invalid = true
299
- query.error = exc.inspect
300
- raise exc unless options[:capture_error]
331
+ error = exc
301
332
  end
302
333
  result
303
- end
304
-
305
- class Query
306
- attr_accessor :conn, :sql, :options
307
- attr_accessor :sql_prepared
308
- attr_accessor :error, :cmd_status_raw, :cmd_status, :cmd_tuples
309
- attr_accessor :nfields, :fields, :ftypes, :fmods
310
- attr_accessor :type_names
311
- attr_accessor :columns, :rows
312
-
313
- def run!
314
- @error = nil
315
- sql = @sql_prepared = prepare_sql self.sql
316
- if conn.verbose || options[:verbose]
317
- out = conn.verbose_stream
318
- out.puts "\n-- =================================================================== --"
319
- out.puts sql
320
- out.puts "-- ==== --"
321
- end
322
- return self if options[:dry_run]
323
- if result = conn.run_query!(sql, self, options)
324
- extract_results! result
325
- end
326
- self
327
- end
328
-
329
- def prepare_sql sql
330
- sql = sql.sub(/[\s\n]*;[\s\n]*\Z/, '')
331
- if options.key?(:limit)
332
- sql = conn.with_limit sql, options[:limit]
333
- end
334
- if arguments = options[:arguments]
335
- if values = arguments[:names_and_values]
336
- n = conn.safe_sql(values.keys * ', ')
337
- v = conn.safe_sql(values.keys.map{|k| ":#{k}"} * ', ')
338
- sql = sql_replace_arguments(sql,
339
- :NAMES => n,
340
- :VALUES => v,
341
- :NAMES_AND_VALUES => conn.safe_sql("( #{n} ) VALUES ( #{v} )"),
342
- :SET_VALUES => conn.safe_sql(values.keys.map{|k| "#{conn.escape_identifier(k)} = :#{k}"} * ', '))
343
- arguments = arguments.merge(values)
344
- end
345
- sql = sql_replace_arguments(sql, arguments)
346
- end
347
- sql
348
- end
349
-
350
- def sql_replace_arguments sql, arguments
351
- sql = sql.gsub(/(:(\w+)\b([?]?))/) do | m |
352
- name = $2.to_sym
353
- optional = ! $3.empty?
354
- if arguments.key?(name) || optional
355
- val = arguments[name]
356
- unless optional && val.nil?
357
- val = conn.escape_value(val)
358
- end
359
- conn.verbose_stream.puts " #{name} => #{val}" if options[:verbose_arguments]
360
- val
361
- else
362
- $1
363
- end
364
- end
365
- sql = sql_replace_match sql
366
- end
367
-
368
- def sql_replace_match sql
369
- sql = sql.gsub(/:~\s*\{\{([^\}]+?)\}\}\s*\{\{([^\}]+?)\}\}/) do | m |
370
- expr = $1
371
- val = $2
372
- case expr
373
- when /\A\s*BETWEEN\b/
374
- "(#{val} #{expr})"
375
- when "NULL"
376
- "(#{val} IS NULL)"
377
- else
378
- "(#{val} = #{expr})"
379
- end
380
- end
381
- sql
382
- end
383
-
384
- def extract_results! result
385
- error = result.error_message
386
- error &&= ! error.empty? && error
387
- @error = error
388
- @cmd_status_raw = result.cmd_status
389
- @cmd_tuples = result.cmd_tuples
390
- @nfields = result.nfields
391
- @ntuples = result.ntuples
392
- @fields = result.fields
393
- @ftypes = (0 ... nfields).map{|i| result.ftype(i) }
394
- @fmods = (0 ... nfields).map{|i| result.fmod(i) }
395
- @rows = result.to_a
396
- result.clear
397
- self
398
- end
399
-
400
- def columns
401
- @columns ||= @fields.zip(type_names)
402
- end
403
-
404
- def type_names
405
- @type_names ||= (0 ... nfields).map{|i| conn.type_name(@ftypes[i], @fmods[i])}
406
- end
407
-
408
- def cmd_status
409
- @cmd_status ||=
410
- begin
411
- x = @cmd_status_raw.split(/\s+/)
412
- [ x[0] ] + x[1 .. -1].map(&:to_i)
413
- end.freeze
414
- end
415
-
416
- def rows
417
- return @rows if @rows_unescaped
418
- @rows.each do | r |
419
- columns.each do | c, t |
420
- r[c] = conn.unescape_value(r[c], t)
421
- end
422
- end
423
- @rows_unescaped = true
424
- @rows
334
+ ensure
335
+ if error
336
+ # $stderr.puts " ERROR: #{exc.inspect}\n #{exc.backtrace * "\n "}"
337
+ query.error = error.inspect
338
+ raise error unless options[:capture_error]
425
339
  end
426
-
427
340
  end
341
+
428
342
  end
429
343
  end
430
344
 
345
+ require 'qreport/connection/query'
@@ -0,0 +1,138 @@
1
+ require 'qreport/connection'
2
+
3
+ module Qreport
4
+ class Connection
5
+ class Query
6
+ attr_accessor :conn, :sql, :options
7
+ attr_accessor :sql_prepared
8
+ attr_accessor :error, :cmd_status_raw, :cmd_status, :cmd_tuples
9
+ attr_accessor :nfields, :fields, :ftypes, :fmods
10
+ attr_accessor :type_names
11
+ attr_accessor :columns, :rows
12
+
13
+ def run!
14
+ @error = nil
15
+ @fields = @ftypes = @mods = EMPTY_Array
16
+ @nfields = 0
17
+ sql = @sql_prepared = prepare_sql self.sql
18
+ if conn.verbose || options[:verbose]
19
+ out = conn.verbose_stream
20
+ out.puts "\n-- =================================================================== --"
21
+ out.puts sql
22
+ out.puts "-- ==== --"
23
+ end
24
+ return self if options[:dry_run]
25
+ if result = conn.run_query!(sql, self, options)
26
+ extract_results! result
27
+ end
28
+ self
29
+ ensure
30
+ @conn = nil
31
+ end
32
+
33
+ def prepare_sql sql
34
+ sql = sql.sub(/[\s\n]*;[\s\n]*\Z/, '')
35
+ if options.key?(:limit)
36
+ sql = conn.with_limit sql, options[:limit]
37
+ end
38
+ if arguments = options[:arguments]
39
+ if values = arguments[:names_and_values]
40
+ n = conn.safe_sql(values.keys * ', ')
41
+ v = conn.safe_sql(values.keys.map{|k| ":#{k}"} * ', ')
42
+ sql = sql_replace_arguments(sql,
43
+ :NAMES => n,
44
+ :VALUES => v,
45
+ :NAMES_AND_VALUES => conn.safe_sql("( #{n} ) VALUES ( #{v} )"),
46
+ :SET_VALUES => conn.safe_sql(values.keys.map{|k| "#{conn.escape_identifier(k)} = :#{k}"} * ', '))
47
+ arguments = arguments.merge(values)
48
+ end
49
+ sql = sql_replace_arguments(sql, arguments)
50
+ end
51
+ sql
52
+ end
53
+
54
+ def sql_replace_arguments sql, arguments
55
+ sql = sql.gsub(/(:(\w+)\b([?]?))/) do | m |
56
+ name = $2.to_sym
57
+ optional = ! $3.empty?
58
+ if arguments.key?(name) || optional
59
+ val = arguments[name]
60
+ unless optional && val.nil?
61
+ val = conn.escape_value(val)
62
+ end
63
+ conn.verbose_stream.puts " #{name} => #{val}" if options[:verbose_arguments]
64
+ val
65
+ else
66
+ $1
67
+ end
68
+ end
69
+ sql = sql_replace_match sql
70
+ end
71
+
72
+ def sql_replace_match sql
73
+ sql = sql.gsub(/:~\s*\{\{([^\}]+?)\}\}\s*\{\{([^\}]+?)\}\}/) do | m |
74
+ expr = $1
75
+ val = $2
76
+ case expr
77
+ when /\A\s*BETWEEN\b/
78
+ "(#{val} #{expr})"
79
+ when "NULL"
80
+ "(#{val} IS NULL)"
81
+ else
82
+ "(#{val} = #{expr})"
83
+ end
84
+ end
85
+ sql
86
+ end
87
+
88
+ def extract_results! result
89
+ error = result.error_message
90
+ error = nil if error.empty?
91
+ @error = error
92
+ @cmd_status_raw = result.cmd_status
93
+ @cmd_tuples = result.cmd_tuples
94
+ @nfields = result.nfields
95
+ @ntuples = result.ntuples
96
+ @fields = result.fields
97
+ @ftypes = (0 ... nfields).map{|i| result.ftype(i) }
98
+ @fmods = (0 ... nfields).map{|i| result.fmod(i) }
99
+ @rows = result.to_a
100
+ type_names
101
+ rows
102
+ self
103
+ ensure
104
+ result.clear
105
+ @conn = nil
106
+ end
107
+
108
+ def columns
109
+ @columns ||= @fields.zip(type_names)
110
+ end
111
+
112
+ def type_names
113
+ @type_names ||= (0 ... nfields).map{|i| @conn.type_name(@ftypes[i], @fmods[i])}
114
+ end
115
+
116
+ def cmd_status
117
+ @cmd_status ||=
118
+ begin
119
+ x = @cmd_status_raw.split(/\s+/)
120
+ [ x[0] ] + x[1 .. -1].map(&:to_i)
121
+ end.freeze
122
+ end
123
+
124
+ def rows
125
+ return @rows if @rows_unescaped
126
+ (@rows ||= [ ]).each do | r |
127
+ columns.each do | c, t |
128
+ r[c] = @conn.unescape_value(r[c], t)
129
+ end
130
+ end
131
+ @rows_unescaped = true
132
+ @rows
133
+ end
134
+
135
+ end
136
+ end
137
+ end
138
+
@@ -182,8 +182,7 @@ END
182
182
 
183
183
  # Return rows from this report run's report table.
184
184
  def data
185
- @data ||=
186
- _select
185
+ @data ||= ReportRun::Data.new(self)
187
186
  end
188
187
 
189
188
  def select options = nil
@@ -229,3 +228,5 @@ END
229
228
  end
230
229
  end
231
230
  end
231
+
232
+ require 'qreport/report_run/data'
@@ -0,0 +1,33 @@
1
+ require 'qreport/report_run'
2
+
3
+ module Qreport
4
+ class ReportRun
5
+ # Delays pulling in entire result set to determine columns of report result table.
6
+ class Data
7
+ attr_accessor :report_run
8
+
9
+ def initialize report_run; @report_run = report_run; end
10
+
11
+ def columns; _select0.columns; end
12
+ def type_names; _select0.type_names; end
13
+ def rows; _select.rows; end
14
+ def error; _select0.error; end
15
+
16
+ # Delegate all other methods to the Connection::Query object.
17
+ def method_missing sel, *args, &blk
18
+ _select.send(sel, *args, &blk)
19
+ end
20
+
21
+ private
22
+
23
+ def _select
24
+ @_select ||= report_run._select(:capture_error => true)
25
+ end
26
+
27
+ def _select0
28
+ @_select0 ||= (@_select || report_run._select(:capture_error => true, :limit => 0))
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module Qreport
2
- VERSION = "0.0.10"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -2,18 +2,16 @@ require 'spec_helper'
2
2
  require 'qreport/connection'
3
3
 
4
4
  describe Qreport::Connection do
5
- QREPORT_TEST_CONN = [ nil ]
6
- def conn
7
- QREPORT_TEST_CONN[0] ||= Qreport::Connection.new
8
- end
5
+ let(:conn) { Qreport::Connection.new }
6
+ after(:each) { conn._close }
9
7
 
10
8
  it "can to connect to a test database." do
11
9
  conn.conn.class.should == PG::Connection
12
10
  end
13
11
 
14
12
  it "can manage transaction state." do
15
- conn.should_receive(:_transaction_begin).once
16
- conn.should_receive(:_transaction_commit).once
13
+ conn.should_receive(:_transaction_begin).exactly(1).times
14
+ conn.should_receive(:_transaction_commit).exactly(1).times
17
15
  conn.should_receive(:_transaction_abort).exactly(0).times
18
16
  conn.in_transaction?.should == false
19
17
  conn.transaction do
@@ -23,8 +21,8 @@ describe Qreport::Connection do
23
21
  end
24
22
 
25
23
  it "can manage nested transactions." do
26
- conn.should_receive(:_transaction_begin).once
27
- conn.should_receive(:_transaction_commit).once
24
+ conn.should_receive(:_transaction_begin).exactly(1).times
25
+ conn.should_receive(:_transaction_commit).exactly(1).times
28
26
  conn.should_receive(:_transaction_abort).exactly(0).times
29
27
  conn.in_transaction?.should == false
30
28
  conn.transaction do
@@ -38,9 +36,9 @@ describe Qreport::Connection do
38
36
  end
39
37
 
40
38
  it "can manage transaction state during raised exceptions" do
41
- conn.should_receive(:_transaction_begin).once
39
+ conn.should_receive(:_transaction_begin).exactly(1).times
42
40
  conn.should_receive(:_transaction_commit).exactly(0).times
43
- conn.should_receive(:_transaction_abort).once
41
+ conn.should_receive(:_transaction_abort).exactly(1).times
44
42
  lambda do
45
43
  conn.transaction do
46
44
  raise Qreport::Error, "#{__LINE__}"
@@ -49,6 +47,19 @@ describe Qreport::Connection do
49
47
  conn.in_transaction?.should == false
50
48
  end
51
49
 
50
+
51
+ it "will error upon #close during transaction" do
52
+ conn.should_receive(:_transaction_begin).exactly(1).times
53
+ conn.should_receive(:_transaction_commit).exactly(0).times
54
+ conn.should_receive(:_transaction_abort).exactly(1).times
55
+ lambda do
56
+ conn.transaction do
57
+ conn.close
58
+ end
59
+ end.should raise_error(Qreport::Error)
60
+ conn.in_transaction?.should == false
61
+ end
62
+
52
63
  it "can dup to create another connection." do
53
64
  conn1 = Qreport::Connection.new
54
65
  conn1.fd.should == nil
@@ -130,6 +141,14 @@ describe Qreport::Connection do
130
141
  [ Time.parse('2011-04-27 13:23:00 -0500'), "'2011-04-27T13:23:00.000000-05:00'::timestamp", Time.parse('2011-04-27 13:23:00 -0500') ],
131
142
  [ :IGNORE, "'13:23'::time", '13:23:00' ],
132
143
  [ [ 1, "2", :three ], "'[1,\"2\",\"three\"]'", :IGNORE ],
144
+ =begin
145
+ DOES NOT WORK YET
146
+ [ [ 1, 2, 3 ], 'ARRAY[1,2,3]', ],
147
+ [ [ 1, 2, nil, 3 ], 'ARRAY[1,2,NULL,3]', ],
148
+ [ [ 1, 2.2, 3 ], 'ARRAY[1,2.2,3]', [ 1.0, 2.2, 3.0 ] ],
149
+ [ [ 1, nil, 2.2, 3 ], 'ARRAY[1,NULL,2.2,3]', [ 1.0, nil, 2.2, 3.0 ] ],
150
+ [ :IGNORE, 'ARRAY[1,NULL,2.2,3]', [ 1.0, nil, 2.2, 3.0 ] ],
151
+ =end
133
152
  [ { :a => 1, "b" => 2 }, "'{\"a\":1,\"b\":2}'", :IGNORE ],
134
153
  ].each do | value, sql, return_value, sql_expr, sql_value |
135
154
  if value != :IGNORE
@@ -56,6 +56,7 @@ describe Qreport::ReportRunner do
56
56
  r2.data.columns.should == r.data.columns
57
57
  r2.data.rows.should == r.data.rows
58
58
  r2.data.rows.size.should == r.nrows
59
+ r2.data.cmd_status[0].should == "SELECT"
59
60
  end
60
61
  end
61
62
 
@@ -106,7 +107,6 @@ FROM articles
106
107
  SELECT total_users.*, total_articles.*
107
108
  FROM total_users, total_articles;
108
109
  END
109
- report_run.verbose = true
110
110
  report_run.run! conn
111
111
  report_run.columns.should == [["qr_run_id", "bigint"], ["qr_run_row", "bigint"], ["total_users", "bigint"], ["total_articles", "bigint"]]
112
112
  rows = report_run.select.rows
@@ -165,7 +165,6 @@ END
165
165
 
166
166
  [ '1 days', '2 days', '30 days', '60 days' ].each do | interval |
167
167
  report_run = Qreport::ReportRun.new(:name => :users_with_articles, :description => interval, :variant => interval)
168
- report_run.verbose = verbose
169
168
  report_run.arguments = {
170
169
  :now => now,
171
170
  :interval => interval,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qreport
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-01 00:00:00.000000000 Z
12
+ date: 2013-10-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -140,10 +140,12 @@ files:
140
140
  - Rakefile
141
141
  - lib/qreport.rb
142
142
  - lib/qreport/connection.rb
143
+ - lib/qreport/connection/query.rb
143
144
  - lib/qreport/initialization.rb
144
145
  - lib/qreport/main.rb
145
146
  - lib/qreport/model.rb
146
147
  - lib/qreport/report_run.rb
148
+ - lib/qreport/report_run/data.rb
147
149
  - lib/qreport/report_runner.rb
148
150
  - lib/qreport/version.rb
149
151
  - qreport.gemspec
@@ -164,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
166
  version: '0'
165
167
  segments:
166
168
  - 0
167
- hash: -862269867070365342
169
+ hash: 2467578902340912211
168
170
  required_rubygems_version: !ruby/object:Gem::Requirement
169
171
  none: false
170
172
  requirements:
@@ -173,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
175
  version: '0'
174
176
  segments:
175
177
  - 0
176
- hash: -862269867070365342
178
+ hash: 2467578902340912211
177
179
  requirements: []
178
180
  rubyforge_project:
179
181
  rubygems_version: 1.8.25