qreport 0.0.7 → 0.0.8

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