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 +11 -0
- data/README.md +2 -2
- data/lib/qreport/connection.rb +59 -27
- data/lib/qreport/report_run.rb +3 -0
- data/lib/qreport/version.rb +1 -1
- data/spec/lib/qreport/connection_spec.rb +49 -7
- data/spec/lib/qreport/report_runner_spec.rb +10 -3
- metadata +4 -4
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
|
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
|
126
|
+
$ PGHOST=localhost PGUSER=test PGDATABASE=test PGPASSWORD=... rake
|
127
127
|
|
128
128
|
## Contributing
|
129
129
|
|
data/lib/qreport/connection.rb
CHANGED
@@ -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, :
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
239
|
+
IDENTITY
|
228
240
|
end
|
229
241
|
end
|
242
|
+
IDENTITY = lambda { | val, type | val }
|
230
243
|
|
231
|
-
def
|
232
|
-
|
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
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
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
|
-
|
359
|
+
verbose_stream.puts " #{name} => #{val}" if options[:verbose_arguments]
|
328
360
|
val
|
329
361
|
else
|
330
362
|
$1
|
data/lib/qreport/report_run.rb
CHANGED
@@ -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;
|
data/lib/qreport/version.rb
CHANGED
@@ -86,7 +86,34 @@ describe Qreport::Connection do
|
|
86
86
|
conn.instance_variable_get('@conn').should == nil
|
87
87
|
end
|
88
88
|
|
89
|
-
describe "#
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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 ==
|
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
|
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
|
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
|
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.
|
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-
|
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:
|
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:
|
176
|
+
hash: 3423526995074197651
|
177
177
|
requirements: []
|
178
178
|
rubyforge_project:
|
179
179
|
rubygems_version: 1.8.25
|