qreport 0.0.7 → 0.0.8

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-09-24 Kurt A. Stephens <ks.github@kurtstephens.com>
2
+ * v0.0.8: Bug fixes, performance improvements, new features.
3
+ * Correctly handle other Float column types.
4
+ * User-defined unescape_value functions.
5
+ * User-defined verbose output stream.
6
+ * Ephermeral ReportRun#error_object.
7
+
8
+ 2013-07-10 Kurt A. Stephens <ks.github@kurtstephens.com>
9
+ * v0.0.7: Bug fix.
10
+ * Do whatever it takes to DROP SEQUENCE IF EXISTS qr_row_seq for pooled connections.
11
+
1
12
  2013-07-08 Kurt A. Stephens <ks.github@kurtstephens.com>
2
13
 
3
14
  * v0.0.6: New Version: New Functionality.
data/README.md CHANGED
@@ -108,7 +108,7 @@ Example: a Range of Time values matching a.created_on:
108
108
  :interval => (t - 86400) ... t,
109
109
  }
110
110
  report_run.run! <<"END"
111
- SELECT * FROM articles a WHERe :~ {{:interval}} {{a.created_on}}
111
+ SELECT * FROM articles a WHERE :~ {{:interval}} {{a.created_on}}
112
112
  END
113
113
 
114
114
  ## Batch Processing
@@ -123,7 +123,7 @@ Example setup:
123
123
  postgres=# create database test owner test;
124
124
  CREATE DATABASE
125
125
  postgres=# \q
126
- $ PGHOST=localhost PGUSER=test PGDATABSE=test PGPASSWORD=... rake
126
+ $ PGHOST=localhost PGUSER=test PGDATABASE=test PGPASSWORD=... rake
127
127
 
128
128
  ## Contributing
129
129
 
@@ -1,11 +1,14 @@
1
1
  require 'qreport'
2
2
  require 'time' # iso8601
3
+ require 'pp' # dump_result!
3
4
 
4
5
  module Qreport
5
6
  class Connection
6
- attr_accessor :arguments, :verbose, :verbose_result, :env
7
+ attr_accessor :arguments, :env
8
+ attr_accessor :verbose, :verbose_result, :verbose_stream
7
9
  attr_accessor :schemaname
8
10
  attr_accessor :conn, :conn_owned
11
+ attr_accessor :unescape_value_funcs
9
12
 
10
13
  class << self
11
14
  attr_accessor :current
@@ -22,6 +25,7 @@ module Qreport
22
25
  def initialize_copy src
23
26
  @conn = @conn_owned = nil
24
27
  @abort_transaction = @invalid = nil
28
+ @unescape_value_funcs_cache = nil
25
29
  @transaction_nesting = 0
26
30
  end
27
31
 
@@ -158,7 +162,7 @@ module Qreport
158
162
  result.options = options
159
163
  result.conn = self
160
164
  result.run!
161
- dump_result result if @verbose_result || options[:verbose_result]
165
+ dump_result! result if @verbose_result || options[:verbose_result]
162
166
  result
163
167
  end
164
168
 
@@ -198,7 +202,7 @@ module Qreport
198
202
  when Hash, Array
199
203
  escape_value(val.to_json)
200
204
  else
201
- raise TypeError
205
+ raise TypeError, "cannot escape_value on #{val.class.name}"
202
206
  end.to_s
203
207
  end
204
208
  NULL = 'NULL'.freeze
@@ -209,35 +213,62 @@ module Qreport
209
213
 
210
214
  def unescape_value val, type
211
215
  case val
216
+ when nil
212
217
  when String
213
218
  return nil if val == NULL
214
- case type
215
- when "boolean"
216
- val = val == T
217
- when /int/
218
- val = val.to_i
219
- when "numeric"
220
- val = val.to_f
221
- when /timestamp/
222
- val = Time.parse(val)
223
- else
224
- val
225
- end
219
+ func = (@unescape_value_funcs_cache ||= { })[type] ||= unescape_value_func(type)
220
+ val = func.call(val, type)
221
+ end
222
+ val
223
+ end
224
+
225
+ def unescape_value_func type
226
+ if @unescape_value_funcs and func = @unescape_value_funcs[type]
227
+ return func
228
+ end
229
+ case type
230
+ when /^bool/
231
+ lambda { | val, type | val == T }
232
+ when /^(int|smallint|bigint|oid|tid|xid|cid)/
233
+ lambda { | val, type | val.to_i }
234
+ when /^(float|real|double|numeric)/
235
+ lambda { | val, type | val.to_f }
236
+ when /^timestamp/
237
+ lambda { | val, type | Time.parse(val) }
226
238
  else
227
- val
239
+ IDENTITY
228
240
  end
229
241
  end
242
+ IDENTITY = lambda { | val, type | val }
230
243
 
231
- def dump_result result
232
- pp result if result
244
+ def verbose_stream
245
+ @verbose_stream || $stderr
246
+ end
247
+
248
+ def dump_result! result, stream = nil
249
+ PP.pp(result, stream || verbose_stream) if result
233
250
  result
234
251
  end
235
252
 
236
- def type_name type, mod
237
- @type_names ||= { }
238
- @type_names[[type, mod]] ||=
239
- conn.exec("SELECT format_type($1,$2)", [type, mod]).
240
- getvalue(0, 0).to_s.dup.freeze
253
+ # Returns a frozen String representing a column type.
254
+ # The String also responds to #pg_ftype and #pg_fmod.
255
+ def type_name *args
256
+ (@type_names ||= { })[args] ||=
257
+ _type_name(args)
258
+ end
259
+
260
+ module TypeName
261
+ attr_accessor :pg_ftype, :pg_fmod
262
+ end
263
+
264
+ def _type_name args
265
+ x = conn.exec("SELECT pg_catalog.format_type($1,$2)", args).
266
+ getvalue(0, 0).to_s.dup
267
+ # x = ":#{args * ','}" if x.empty? or x == "unknown"
268
+ x.extend(TypeName)
269
+ x.pg_ftype, x.pg_fmod = args
270
+ x.freeze
271
+ x
241
272
  end
242
273
 
243
274
  def with_limit sql, limit = nil
@@ -283,9 +314,10 @@ module Qreport
283
314
  @error = nil
284
315
  sql = @sql_prepared = prepare_sql self.sql
285
316
  if conn.verbose || options[:verbose]
286
- $stderr.puts "\n-- =================================================================== --"
287
- $stderr.puts sql
288
- $stderr.puts "-- ==== --"
317
+ out = verbose_stream
318
+ out.puts "\n-- =================================================================== --"
319
+ out.puts sql
320
+ out.puts "-- ==== --"
289
321
  end
290
322
  return self if options[:dry_run]
291
323
  if result = conn.run_query!(sql, self, options)
@@ -324,7 +356,7 @@ module Qreport
324
356
  unless optional && val.nil?
325
357
  val = conn.escape_value(val)
326
358
  end
327
- $stderr.puts " #{name} => #{val}" if options[:verbose_arguments]
359
+ verbose_stream.puts " #{name} => #{val}" if options[:verbose_arguments]
328
360
  val
329
361
  else
330
362
  $1
@@ -80,12 +80,15 @@ module Qreport
80
80
  when String
81
81
  @error = JSON.parse(x)
82
82
  when Exception
83
+ @error_object = x
83
84
  @error = { :error_class => x.class.name, :error_message => x.message }
84
85
  else
85
86
  raise TypeError
86
87
  end
87
88
  end
88
89
 
90
+ def error_object; @error_object || @error; end
91
+
89
92
  def self.schema! conn, options = { }
90
93
  result = conn.run <<"END", options.merge(:capture_error => true) # , :verbose => true
91
94
  CREATE SEQUENCE qr_report_runs_pkey;
@@ -1,3 +1,3 @@
1
1
  module Qreport
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -86,7 +86,34 @@ describe Qreport::Connection do
86
86
  conn.instance_variable_get('@conn').should == nil
87
87
  end
88
88
 
89
- describe "#escape_value, #unescape_value" do
89
+ describe "#unescape_value" do
90
+ it "should not alter undefined types" do
91
+ conn.unescape_value(123, :UNDEFINED1).should == 123
92
+ conn.unescape_value("str", :UNDEFINED1).should == "str"
93
+ conn.unescape_value(:sym, :UNDEFINED1).should == :sym
94
+ end
95
+
96
+ it "should handle boolean" do
97
+ conn.unescape_value("t", 'boolean').should == true
98
+ conn.unescape_value("f", 'boolean').should == false
99
+ conn.unescape_value(true, 'boolean').should == true
100
+ conn.unescape_value(false, 'boolean').should == false
101
+ end
102
+
103
+ it "should handle floats" do
104
+ conn.unescape_value(123, 'float').should == 123
105
+ conn.unescape_value("123.45", 'float').should == 123.45
106
+ conn.unescape_value(123.45, 'float').should == 123.45
107
+ conn.unescape_value("123.45", 'double precision').should == 123.45
108
+ end
109
+
110
+ it "should handle defined types" do
111
+ conn.unescape_value_funcs = { 'money' => lambda { | val, type | [ val ] } }
112
+ conn.unescape_value("123.00", 'money').should == [ "123.00" ]
113
+ end
114
+ end
115
+
116
+ describe "#escape_value/#unescape_value" do
90
117
  [
91
118
  [ nil, 'NULL' ],
92
119
  [ true, "'t'::boolean" ],
@@ -94,22 +121,37 @@ describe Qreport::Connection do
94
121
  [ 1234, '1234' ],
95
122
  [ -1234, '-1234' ],
96
123
  [ 1234.45, '1234.45' ],
124
+ [ :IGNORE, '1234.56::float', 1234.56 ],
125
+ [ :IGNORE, '1234.56::float4', 1234.56 ],
126
+ [ :IGNORE, '1234.56::float8', 1234.56 ],
97
127
  [ "string with \", \\, and \'", "'string with \", \\, and '''" ],
98
128
  [ :a_symbol!, "'a_symbol!'", :a_symbol!.to_s ],
99
129
  [ Time.parse('2011-04-27T13:23:00.000000Z'), "'2011-04-27T13:23:00.000000Z'::timestamp", Time.parse('2011-04-27T13:23:00.000000') ],
100
130
  [ 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
+ [ :IGNORE, "'13:23'::time", '13:23:00' ],
101
132
  [ [ 1, "2", :three ], "'[1,\"2\",\"three\"]'", :IGNORE ],
102
133
  [ { :a => 1, "b" => 2 }, "'{\"a\":1,\"b\":2}'", :IGNORE ],
103
- ].each do | value, sql, return_value |
134
+ ].each do | value, sql, return_value, sql_expr, sql_value |
135
+ if value != :IGNORE
104
136
  it "can handle encoding #{value.class.name} value #{value.inspect} as #{sql.inspect}." do
105
137
  conn.escape_value(value).should == sql
106
138
  end
107
- it "can handle decoding #{value.class.name} value #{value.inspect}." do
108
- pending :if => return_value == :IGNORE
109
- sql_x = conn.escape_value(value)
110
- r = conn.run "SELECT #{sql_x}"
139
+ end
140
+
141
+ sql_value = return_value
142
+ sql_value = nil if sql_value == :IGNORE
143
+ sql_value ||= value
144
+ if return_value != :IGNORE
145
+ it "can handle decoding #{sql.inspect} as #{sql_value.inspect}." do
146
+ sql_x = sql # conn.escape_value(sql)
147
+ r = conn.run %Q{SELECT #{sql_x} AS "value"}
148
+ # PP.pp r.columns
149
+ # PP.pp r.ftypes
150
+ # PP.pp r.fmods
111
151
  r = r.rows.first.values.first
112
- r.should == (return_value || value)
152
+ r.should == sql_value
153
+ r.class.should == sql_value.class
154
+ end
113
155
  end
114
156
  end
115
157
  it "raises TypeError for other values." do
@@ -21,7 +21,11 @@ describe Qreport::ReportRunner do
21
21
  r.select(:limit => [ 4, 2 ]).rows.map{|x| x["user_id"]}.should == [ 3, 4, 5, 6 ]
22
22
 
23
23
  r = reports['60 days']
24
- r.select.rows.map{|x| x["user_id"]}.should == (1..10).to_a
24
+ rows = r.select.rows
25
+ rows.map{|x| x["user_id"]}.should == (1..10).to_a
26
+
27
+ r.columns.should == [["qr_run_id", "bigint"], ["qr_run_row", "bigint"], ["user_id", "integer"], ["user_rank", "double precision"]]
28
+ rows.map{|x| x["user_rank"].class}.should == [ Float ] * 10
25
29
 
26
30
  reports.values.each do | r |
27
31
  r.delete!
@@ -76,8 +80,9 @@ describe Qreport::ReportRunner do
76
80
  EXISTS(SELECT * FROM articles a WHERE a.user_id = u.id AND a.created_on >= :now - INTERVAL :interval)
77
81
  END
78
82
  report_run.run! conn
83
+ report_run.error_object.should be_a PG::Error
79
84
  report_run.error.class.should == Hash
80
- report_run.error[:error_class].should == 'PG::Error'
85
+ report_run.error[:error_class].should =~ /^PG::/
81
86
  report_run.error[:error_message].should =~ /column "unknown_column" does not exist/
82
87
 
83
88
  report_run.delete!
@@ -105,7 +110,9 @@ END
105
110
  @reports = { }
106
111
 
107
112
  sql = <<"END"
108
- SELECT u.id AS "user_id"
113
+ SELECT
114
+ u.id AS "user_id"
115
+ , u.id / (SELECT MAX(id) FROM users)::float as "user_rank"
109
116
  FROM users u
110
117
  WHERE
111
118
  EXISTS(SELECT * FROM articles a WHERE a.user_id = u.id AND a.created_on >= :now - 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.7
4
+ version: 0.0.8
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-07-11 00:00:00.000000000 Z
12
+ date: 2013-09-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -164,7 +164,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
164
  version: '0'
165
165
  segments:
166
166
  - 0
167
- hash: 2296611255217817535
167
+ hash: 3423526995074197651
168
168
  required_rubygems_version: !ruby/object:Gem::Requirement
169
169
  none: false
170
170
  requirements:
@@ -173,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
173
  version: '0'
174
174
  segments:
175
175
  - 0
176
- hash: 2296611255217817535
176
+ hash: 3423526995074197651
177
177
  requirements: []
178
178
  rubyforge_project:
179
179
  rubygems_version: 1.8.25