qreport 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require 'qreport'
2
+
3
+ module Qreport
4
+ module Initialization
5
+ def initialize opts = nil
6
+ opts ||= EMPTY_Hash
7
+ initialize_before_opts if respond_to? :initialize_before_opts
8
+ opts.each do | k, v |
9
+ send(:"#{k}=", v)
10
+ end
11
+ initialize_after_opts if respond_to? :initialize_after_opts
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require 'qreport/report_run'
2
+ require 'qreport/report_runner'
3
+ require 'pp'
4
+
5
+ module Qreport
6
+ class Main
7
+ end
8
+ end
9
+
@@ -0,0 +1,8 @@
1
+ module Qreport
2
+ module Model
3
+ attr_accessor :conn
4
+ def conn
5
+ @conn || Qreport::Connection.current
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,191 @@
1
+ require 'qreport'
2
+ require 'qreport/model'
3
+ require 'qreport/initialization'
4
+
5
+ module Qreport
6
+ class ReportRun
7
+ include Model, Initialization
8
+
9
+ attr_accessor :id
10
+ attr_accessor :name, :sql, :additional_columns
11
+ attr_accessor :description
12
+ attr_accessor :arguments
13
+ attr_accessor :report_id
14
+ attr_accessor :report_sql
15
+ attr_accessor :columns, :base_columns, :column_signature
16
+ attr_accessor :report_table
17
+ attr_accessor :nrows
18
+ attr_accessor :created_at, :started_at, :finished_at
19
+
20
+ # Construct report_table name from column names and types.
21
+ def report_table
22
+ @report_table ||=
23
+ "#{name}_#{column_signature}".
24
+ gsub(/[^a-z0-9_]/i, '_').freeze
25
+ end
26
+
27
+ def column_signature
28
+ @column_signature ||=
29
+ begin
30
+ return @column_signature = "ERROR" if base_columns.empty?
31
+ column_signature_string = columns.to_json
32
+ @column_signature_hash = Digest::MD5.hexdigest(column_signature_string)
33
+ Base64.strict_encode64(Digest::MD5.digest(column_signature_string)).
34
+ sub(/=+$/, '').
35
+ gsub(/[^a-z0-9]/i, '_').
36
+ downcase.
37
+ freeze
38
+ end
39
+ end
40
+
41
+ def base_columns
42
+ @base_columns ||= EMPTY_Array
43
+ end
44
+
45
+ def additional_columns
46
+ @additional_columns ||= EMPTY_Array
47
+ end
48
+
49
+ def columns
50
+ @columns ||=
51
+ base_columns +
52
+ additional_columns.map{|x| x.map(&:to_s)}
53
+ end
54
+
55
+ def run! conn
56
+ runner = Qreport::ReportRunner.new
57
+ runner.connection = conn
58
+ runner.run!(self)
59
+ self
60
+ end
61
+
62
+ def error
63
+ self.error = @error if String === @error
64
+ @error
65
+ end
66
+
67
+ def error= x
68
+ case x
69
+ when nil, Hash
70
+ @error = x
71
+ when String
72
+ @error = JSON.parse(x)
73
+ when Exception
74
+ @error = { :error_class => x.class.name, :error_message => x.message }
75
+ else
76
+ raise TypeError
77
+ end
78
+ end
79
+
80
+ def self.schema! conn, options = { }
81
+ result = conn.run <<"END", options.merge(:capture_error => true) # , :verbose => true
82
+ CREATE SEQUENCE qr_report_runs_pkey;
83
+ CREATE TABLE -- IF NOT EXISTS
84
+ qr_report_runs (
85
+ id INTEGER PRIMARY KEY DEFAULT nextval('qr_report_runs_pkey')
86
+ , name VARCHAR(255) NOT NULL
87
+ , sql TEXT NOT NULL
88
+ , description TEXT NOT NULL
89
+ , arguments TEXT NOT NULL
90
+ , base_columns TEXT NOT NULL
91
+ , additional_columns TEXT NOT NULL
92
+ , report_table VARCHAR(255) NOT NULL
93
+ , error TEXT
94
+ , created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
95
+ , started_at TIMESTAMP WITH TIME ZONE
96
+ , finished_at TIMESTAMP WITH TIME ZONE
97
+ , nrows INTEGER
98
+ );
99
+ CREATE INDEX qr_report_runs__name ON qr_report_runs (name);
100
+ CREATE INDEX qr_report_runs__report_table ON qr_report_runs (report_table);
101
+ CREATE INDEX qr_report_runs__created_at ON qr_report_runs (created_at);
102
+ END
103
+ end
104
+
105
+ def insert!
106
+ values = {
107
+ :name => name,
108
+ :sql => sql,
109
+ :description => description,
110
+ :arguments => (arguments || { }),
111
+ :base_columns => base_columns,
112
+ :additional_columns => additional_columns,
113
+ :report_table => report_table,
114
+ :error => error,
115
+ :created_at => created_at,
116
+ :started_at => started_at,
117
+ :finished_at => finished_at,
118
+ :nrows => nrows,
119
+ }
120
+
121
+ result = conn.run 'INSERT INTO qr_report_runs ( :NAMES ) VALUES ( :VALUES ) RETURNING id',
122
+ :arguments => { :names_and_values => values } # , :verbose => true, :verbose_arguments => true
123
+ self.id = result.rows[0]["id"] or raise "no id"
124
+
125
+ self
126
+ end
127
+
128
+ def _options options
129
+ options ||= EMPTY_Hash
130
+ arguments = options[:arguments] || EMPTY_Hash
131
+ arguments = arguments.merge(:qr_run_id => id)
132
+ options.merge(:arguments => arguments)
133
+ end
134
+
135
+ def update! options = nil
136
+ options = _options options
137
+ values = options[:values] || EMPTY_Hash
138
+ if Array === values
139
+ h = { }
140
+ values.each { | k | h[k] = send(k) }
141
+ values = h
142
+ end
143
+ options[:arguments].update(:names_and_values => values)
144
+ # options.update :verbose => true, :verbose_result => true # , :dry_run => true
145
+ conn.run <<"END", options
146
+ UPDATE qr_report_runs
147
+ SET :SET_VALUES
148
+ WHERE id = :qr_run_id
149
+ :WHERE?
150
+ END
151
+ end
152
+
153
+ def select options = nil
154
+ options = _options options
155
+ _select({:order_by => 'ORDER BY qr_run_row'}.merge(options))
156
+ end
157
+
158
+ def _select options = nil
159
+ options = _options options
160
+ columns = options[:COLUMNS] || '*'
161
+ columns = conn.safe_sql(columns)
162
+ order_by = conn.safe_sql(options[:order_by] || '')
163
+ options[:arguments].update(
164
+ :COLUMNS => columns,
165
+ :ORDER_BY => order_by)
166
+ conn.run "SELECT :COLUMNS FROM #{report_table} WHERE qr_run_id = :qr_run_id :WHERE? :ORDER_BY?", options
167
+ end
168
+
169
+ # Deletes this report and its rows.
170
+ def delete! options = nil
171
+ truncate!
172
+ options = _options options
173
+ options.update(:capture_error => true)
174
+ conn.run "DELETE FROM qr_report_runs WHERE id = :qr_run_id", options # .merge(:verbose => true)
175
+ result =
176
+ conn.run "SELECT COUNT(*) AS \"count\" from qr_report_runs WHERE report_table = :report_table",
177
+ :arguments => { :report_table => report_table }, :capture_error => true # , :verbose => true
178
+ if result.rows[0]["count"] <= 0
179
+ conn.run "-- DROP TABLE #{report_table}", :capture_error => true # , :verbose => true
180
+ end
181
+ end
182
+
183
+ # Deletes the actual rows for this report run.
184
+ def truncate! options = nil
185
+ options = _options options
186
+ options.update(:capture_error => true)
187
+ conn.run "DELETE FROM #{report_table} WHERE qr_run_id = :qr_run_id :WHERE?", options
188
+ self
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,123 @@
1
+ require 'qreport/report_run'
2
+ require 'qreport/connection'
3
+ require 'digest/md5'
4
+ require 'base64'
5
+ require 'json'
6
+ require 'pp'
7
+
8
+ module Qreport
9
+ class ReportRunner
10
+ attr_accessor :connection, :verbose
11
+
12
+ def run! report_run
13
+ report_run.created_at ||=
14
+ report_run.started_at = Time.now.utc
15
+ name = report_run.name
16
+ sql = report_run.sql.strip
17
+
18
+ arguments = report_run.arguments || { }
19
+ error = error_1 = error_2 = nrows = nil
20
+
21
+ Connection.current = connection
22
+
23
+ begin
24
+ conn.transaction do
25
+
26
+ # Create a report row sequence:
27
+ run "CREATE TEMPORARY SEQUENCE qr_row_seq"
28
+
29
+ # Rewrite query to create result table rows:
30
+ arguments = arguments.merge(:qr_run_id => conn.safe_sql("nextval('qr_row_seq')"))
31
+ report_run.report_sql = report_sql(sql)
32
+
33
+ # Proof query to infer base columns:
34
+ result = run report_run.report_sql, :limit => 0, :arguments => arguments, :verbose => @verbose
35
+ report_run.base_columns = result.columns
36
+ result = nil
37
+ end # transaction
38
+ rescue ::Exception => exc
39
+ error = error_1 = exc
40
+ end
41
+
42
+ # Construct report_table name from column names and types:
43
+ report_table = report_run.report_table
44
+
45
+ conn.transaction do
46
+ # Create new ReportRun row:
47
+ report_run.insert!
48
+ report_run_id = report_run.id
49
+ arguments[:qr_run_id] = report_run_id
50
+ report_run.report_sql = report_sql(sql)
51
+ end # transaction
52
+
53
+ unless error
54
+ # Run query into report table:
55
+ begin
56
+ conn.transaction do
57
+ unless conn.table_exists? report_table
58
+ run "CREATE TABLE #{report_table} AS #{report_run.report_sql}", :arguments => arguments, :verbose => @verbose
59
+ run "CREATE INDEX #{report_table}_i1 ON #{report_table} (qr_run_id)"
60
+ run "CREATE INDEX #{report_table}_i2 ON #{report_table} (qr_run_row)"
61
+ run "CREATE UNIQUE INDEX #{report_table}_i3 ON #{report_table} (qr_run_id, qr_run_row)"
62
+ report_run.additional_columns.each do | n, t, d |
63
+ run "ALTER TABLE #{report_table} ADD COLUMN #{conn.escape_identifier(n)} #{t} DEFAULT :d", :arguments => { :d => d || nil }
64
+ end
65
+ else
66
+ result =
67
+ run "INSERT INTO #{report_table} #{report_run.report_sql}", :arguments => arguments, :verbose => @verbose
68
+
69
+ # Get the number of report run rows from cmd_status:
70
+ unless cs = result.cmd_status and cs[0] == 'INSERT' and cs[1] == 0 and nrows = cs[2]
71
+ raise Error, "cannot determine nrows"
72
+ end
73
+ end
74
+ # Get the number of report run rows:
75
+ unless nrows || error
76
+ result = report_run._select :COLUMNS => 'COUNT(*) AS "nrows"' #, :verbose => true
77
+ nrows = result.rows[0]["nrows"] || (raise Error, "cannot determine nrows")
78
+ end
79
+ # pp result
80
+ result = nil
81
+ end # transaction
82
+ rescue ::Exception => exc
83
+ error = error_2 = exc
84
+ end # transaction
85
+ end
86
+
87
+ conn.transaction do
88
+ run "DROP SEQUENCE qr_row_seq" unless error_1
89
+
90
+ # Update stats:
91
+ report_run.finished_at = Time.now.utc
92
+ report_run.nrows = nrows.to_i
93
+ report_run.error = error
94
+ report_run.update! :values => [ :nrows, :finished_at, :error ] # , :verbose_result => true
95
+ end # transaction
96
+
97
+ report_run
98
+ end
99
+
100
+ def report_sql sql
101
+ sql = sql.sub(/\ASELECT\s+/im, <<"END"
102
+ SELECT
103
+ :qr_run_id
104
+ AS "qr_run_id"
105
+ , nextval('qr_row_seq')
106
+ AS "qr_run_row"
107
+ ,
108
+ END
109
+ )
110
+ sql
111
+ end
112
+
113
+ def run *args
114
+ # conn.verbose = true
115
+ conn.run *args
116
+ end
117
+
118
+ def connection
119
+ @connection ||= Connection.new
120
+ end
121
+ alias :conn :connection
122
+ end
123
+ end
@@ -0,0 +1,3 @@
1
+ module Qreport
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'qreport/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "qreport"
8
+ gem.version = Qreport::VERSION
9
+ gem.authors = ["Kurt Stephens"]
10
+ gem.email = ["ks.github@kurtstephens.com"]
11
+ gem.description = %q{Automatically creates materialized report tables from a SQL query.}
12
+ gem.summary = %q{Automatically creates materialized report tables from a SQL query.}
13
+ gem.homepage = "http://github.com/kstephens/qreport"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency 'rake', '>= 0.9.0'
21
+ gem.add_development_dependency 'rspec', '~> 2.12.0'
22
+ gem.add_development_dependency 'simplecov', '~> 0.7.1'
23
+
24
+ gem.add_dependency 'pg', '~> 0.14'
25
+ end
@@ -0,0 +1,118 @@
1
+ require 'spec_helper'
2
+ require 'qreport/connection'
3
+
4
+ describe Qreport::Connection do
5
+ QREPORT_TEST_CONN = [ nil ]
6
+ def conn
7
+ QREPORT_TEST_CONN[0] ||= Qreport::Connection.new
8
+ end
9
+
10
+ it "can to connect to a test database." do
11
+ conn.conn.class.should == PG::Connection
12
+ end
13
+
14
+ it "can manage transaction state." do
15
+ conn.should_receive(:_transaction_begin).once
16
+ conn.should_receive(:_transaction_commit).once
17
+ conn.should_receive(:_transaction_abort).exactly(0).times
18
+ conn.in_transaction?.should == false
19
+ conn.transaction do
20
+ conn.in_transaction?.should == true
21
+ end
22
+ conn.in_transaction?.should == false
23
+ end
24
+
25
+ it "can manage nested transactions." do
26
+ conn.should_receive(:_transaction_begin).once
27
+ conn.should_receive(:_transaction_commit).once
28
+ conn.should_receive(:_transaction_abort).exactly(0).times
29
+ conn.in_transaction?.should == false
30
+ conn.transaction do
31
+ conn.in_transaction?.should == true
32
+ conn.transaction do
33
+ conn.in_transaction?.should == true
34
+ end
35
+ conn.in_transaction?.should == true
36
+ end
37
+ conn.in_transaction?.should == false
38
+ end
39
+
40
+ it "can manage transaction state during raised exceptions" do
41
+ conn.should_receive(:_transaction_begin).once
42
+ conn.should_receive(:_transaction_commit).exactly(0).times
43
+ conn.should_receive(:_transaction_abort).once
44
+ lambda do
45
+ conn.transaction do
46
+ raise Qreport::Error, "#{__LINE__}"
47
+ end
48
+ end.should raise_error(Qreport::Error)
49
+ conn.in_transaction?.should == false
50
+ end
51
+
52
+ it "can dup to create another connection." do
53
+ conn1 = Qreport::Connection.new
54
+ conn1.fd.should == nil
55
+ conn1.conn
56
+ conn1.fd.class.should == Fixnum
57
+ conn2 = nil
58
+ conn1.transaction do
59
+ conn1.in_transaction?.should == true
60
+ conn2 = conn1.dup
61
+ conn2.in_transaction?.should == false
62
+ end
63
+ conn1.in_transaction?.should == false
64
+ conn2.fd.should == nil
65
+ conn2.conn
66
+ conn2.fd.class.should == Fixnum
67
+ conn2.fd.should_not == conn1.fd
68
+ conn2.in_transaction?.should == false
69
+ end
70
+
71
+ it 'can set conn.' do
72
+ conn1 = conn
73
+ conn1.conn.class.should == PG::Connection
74
+ conn2 = Qreport::Connection.new(:conn => conn1.conn)
75
+ conn2.conn.object_id.should == conn1.conn.object_id
76
+ conn2.conn_owned.should be_false
77
+ end
78
+
79
+ it 'can close conn.' do
80
+ conn.conn.class.should == PG::Connection
81
+ conn.conn_owned.should_not be_false
82
+ conn.close
83
+ conn.instance_variable_get('@conn').should == nil
84
+ conn.conn_owned.should be_false
85
+ conn.close
86
+ conn.instance_variable_get('@conn').should == nil
87
+ end
88
+
89
+ describe "#escape_value, #unescape_value" do
90
+ [
91
+ [ nil, 'NULL' ],
92
+ [ true, "'t'::boolean" ],
93
+ [ false, "'f'::boolean" ],
94
+ [ 1234, '1234' ],
95
+ [ -1234, '-1234' ],
96
+ [ 1234.45, '1234.45' ],
97
+ [ "string with \", \\, and \'", "'string with \", \\, and '''" ],
98
+ [ :a_symbol!, "'a_symbol!'", :a_symbol!.to_s ],
99
+ [ Time.parse('2011-04-27T13:23:00.000000Z'), "'2011-04-27T13:23:00.000000Z'::timestamp", Time.parse('2011-04-27T13:23:00.000000') ],
100
+ ].each do | value, sql, return_value |
101
+ it "can handle encoding #{value.class.name} value #{value.inspect} as #{sql.inspect}." do
102
+ conn.escape_value(value).should == sql
103
+ end
104
+ it "can handle decoding #{value.class.name} value #{value.inspect}." do
105
+ sql_x = conn.escape_value(value)
106
+ r = conn.run "SELECT #{sql_x}"
107
+ r = r.rows.first.values.first
108
+ r.should == (return_value || value)
109
+ end
110
+ end
111
+ it "raises TypeError for other values." do
112
+ lambda do
113
+ conn.escape_value(Object.new)
114
+ end.should raise_error(TypeError)
115
+ end
116
+ end
117
+
118
+ end