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