qreport 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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