piggly 1.2.0
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/README.markdown +84 -0
- data/Rakefile +19 -0
- data/bin/piggly +245 -0
- data/lib/piggly/compiler/cache.rb +151 -0
- data/lib/piggly/compiler/pretty.rb +67 -0
- data/lib/piggly/compiler/queue.rb +46 -0
- data/lib/piggly/compiler/tags.rb +244 -0
- data/lib/piggly/compiler/trace.rb +91 -0
- data/lib/piggly/compiler.rb +5 -0
- data/lib/piggly/config.rb +43 -0
- data/lib/piggly/filecache.rb +40 -0
- data/lib/piggly/installer.rb +95 -0
- data/lib/piggly/parser/grammar.tt +747 -0
- data/lib/piggly/parser/nodes.rb +319 -0
- data/lib/piggly/parser/parser.rb +11783 -0
- data/lib/piggly/parser/traversal.rb +48 -0
- data/lib/piggly/parser/treetop_ruby19_patch.rb +17 -0
- data/lib/piggly/parser.rb +67 -0
- data/lib/piggly/profile.rb +87 -0
- data/lib/piggly/reporter/html.rb +207 -0
- data/lib/piggly/reporter/piggly.css +187 -0
- data/lib/piggly/reporter/sortable.js +493 -0
- data/lib/piggly/reporter.rb +21 -0
- data/lib/piggly/task.rb +64 -0
- data/lib/piggly/util.rb +28 -0
- data/lib/piggly/version.rb +15 -0
- data/lib/piggly.rb +18 -0
- data/spec/compiler/cache_spec.rb +9 -0
- data/spec/compiler/pretty_spec.rb +9 -0
- data/spec/compiler/queue_spec.rb +3 -0
- data/spec/compiler/rewrite_spec.rb +3 -0
- data/spec/compiler/tags_spec.rb +285 -0
- data/spec/compiler/trace_spec.rb +173 -0
- data/spec/config_spec.rb +58 -0
- data/spec/filecache_spec.rb +70 -0
- data/spec/fixtures/snippets.sql +158 -0
- data/spec/grammar/expression_spec.rb +302 -0
- data/spec/grammar/statements/assignment_spec.rb +70 -0
- data/spec/grammar/statements/exception_spec.rb +52 -0
- data/spec/grammar/statements/if_spec.rb +178 -0
- data/spec/grammar/statements/loop_spec.rb +41 -0
- data/spec/grammar/statements/sql_spec.rb +71 -0
- data/spec/grammar/tokens/comment_spec.rb +58 -0
- data/spec/grammar/tokens/datatype_spec.rb +52 -0
- data/spec/grammar/tokens/identifier_spec.rb +58 -0
- data/spec/grammar/tokens/keyword_spec.rb +44 -0
- data/spec/grammar/tokens/label_spec.rb +40 -0
- data/spec/grammar/tokens/literal_spec.rb +30 -0
- data/spec/grammar/tokens/lval_spec.rb +50 -0
- data/spec/grammar/tokens/number_spec.rb +34 -0
- data/spec/grammar/tokens/sqlkeywords_spec.rb +45 -0
- data/spec/grammar/tokens/string_spec.rb +54 -0
- data/spec/grammar/tokens/whitespace_spec.rb +40 -0
- data/spec/parser_spec.rb +8 -0
- data/spec/profile_spec.rb +5 -0
- data/spec/reporter/html_spec.rb +0 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/spec_suite.rb +5 -0
- metadata +121 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
module Piggly
|
2
|
+
|
3
|
+
#
|
4
|
+
# Coverage is tracked by attaching these compiler-generated tags to various nodes in a stored
|
5
|
+
# procedure's parse tree. These tags each have a unique string identifier which is printed by
|
6
|
+
# various parts of the recompiled stored procedure, and the output is then recognized by
|
7
|
+
# Profile.notice_processor, which calls #ping on the tag corresponding to the printed string.
|
8
|
+
#
|
9
|
+
# After test execution is complete, each AST is walked and Tag values attached to NodeClass
|
10
|
+
# values are used to produce the coverage report
|
11
|
+
#
|
12
|
+
class Tag
|
13
|
+
PATTERN = /[0-9a-f]{16}/
|
14
|
+
|
15
|
+
attr_accessor :id
|
16
|
+
|
17
|
+
def initialize(prefix = nil, id = nil)
|
18
|
+
@id = Digest::MD5.hexdigest(prefix.to_s + (id || object_id).to_s).slice(0, 16)
|
19
|
+
end
|
20
|
+
|
21
|
+
alias to_s id
|
22
|
+
|
23
|
+
# Defined here in case ActiveSupport hasn't defined it on Object
|
24
|
+
def tap
|
25
|
+
yield self
|
26
|
+
self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class EvaluationTag < Tag
|
31
|
+
def initialize(*args)
|
32
|
+
clear
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def type
|
37
|
+
:block
|
38
|
+
end
|
39
|
+
|
40
|
+
def ping(value)
|
41
|
+
@ran = true
|
42
|
+
end
|
43
|
+
|
44
|
+
def style
|
45
|
+
"c#{@ran ? '1' : '0'}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_f
|
49
|
+
@ran ? 100.0 : 0.0
|
50
|
+
end
|
51
|
+
|
52
|
+
def complete?
|
53
|
+
@ran
|
54
|
+
end
|
55
|
+
|
56
|
+
def description
|
57
|
+
@ran ? 'full coverage' : 'never evaluated'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Resets code coverage
|
61
|
+
def clear
|
62
|
+
@ran = false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Sequence of statements
|
68
|
+
#
|
69
|
+
class BlockTag < EvaluationTag
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Procedure calls, raise exception, exits, returns
|
74
|
+
#
|
75
|
+
class UnconditionalBranchTag < EvaluationTag
|
76
|
+
# aggregate this coverage data with conditional branches
|
77
|
+
def type
|
78
|
+
:branch
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class LoopConditionTag < Tag
|
83
|
+
STATES = { # never terminates normally (so @pass must be false)
|
84
|
+
0b0000 => 'condition was never evaluated',
|
85
|
+
0b0001 => 'condition never evaluates false (terminates early). loop always iterates more than once',
|
86
|
+
0b0010 => 'condition never evaluates false (terminates early). loop always iterates only once',
|
87
|
+
0b0011 => 'condition never evaluates false (terminates early)',
|
88
|
+
# terminates normally (one of @pass, @once, @twice must be true)
|
89
|
+
0b1001 => 'loop always iterates more than once',
|
90
|
+
0b1010 => 'loop always iterates only once',
|
91
|
+
0b1011 => 'loop never passes through',
|
92
|
+
0b1100 => 'loop always passes through',
|
93
|
+
0b1101 => 'loop never iterates only once',
|
94
|
+
0b1110 => 'loop never iterates more than once',
|
95
|
+
0b1111 => 'full coverage' }
|
96
|
+
|
97
|
+
attr_reader :pass, :once, :twice, :ends, :count
|
98
|
+
|
99
|
+
def initialize(*args)
|
100
|
+
clear
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
def type
|
105
|
+
:loop
|
106
|
+
end
|
107
|
+
|
108
|
+
def ping(value)
|
109
|
+
case value
|
110
|
+
when 't'
|
111
|
+
# loop iterated
|
112
|
+
@count += 1
|
113
|
+
else
|
114
|
+
# loop terminated
|
115
|
+
case @count
|
116
|
+
when 0; @pass = true
|
117
|
+
when 1; @once = true
|
118
|
+
else; @twice = true
|
119
|
+
end
|
120
|
+
@count = 0
|
121
|
+
|
122
|
+
# this isn't accurate. there needs to be a signal at the end
|
123
|
+
# of the loop body to indicate it was reached. otherwise its
|
124
|
+
# possible each iteration restarts early with 'continue'
|
125
|
+
@ends = true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def style
|
130
|
+
"l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_f
|
134
|
+
# value space:
|
135
|
+
# (1,2,X) - loop iterated at least twice and terminated normally
|
136
|
+
# (1,X) - loop iterated only once and terminated normally
|
137
|
+
# (0,X) - loop never iterated and terminated normally (pass-thru)
|
138
|
+
# () - loop condition was never executed
|
139
|
+
#
|
140
|
+
# these combinations are ignored, because adding tests for them will probably not reveal bugs
|
141
|
+
# (1,2) - loop iterated at least twice but terminated early
|
142
|
+
# (1) - loop iterated only once but terminated early
|
143
|
+
100 * ([@pass, @once, @twice, @ends].count{|x| x } / 4.0)
|
144
|
+
end
|
145
|
+
|
146
|
+
def complete?
|
147
|
+
@pass and @once and @twice and @ends
|
148
|
+
end
|
149
|
+
|
150
|
+
def description
|
151
|
+
# weird hack so ForCollectionTag uses its separate constant
|
152
|
+
self.class::STATES.fetch(n = state, "unknown tag state: #{n}")
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns state represented as a 4-bit integer
|
156
|
+
def state
|
157
|
+
[@ends,@pass,@once,@twice].reverse.inject([0,0]){|(k,n), bit| [k + 1, n | (bit ? 1 : 0) << k] }.last
|
158
|
+
end
|
159
|
+
|
160
|
+
def clear
|
161
|
+
@pass = false
|
162
|
+
@once = false
|
163
|
+
@twice = false
|
164
|
+
@ends = false
|
165
|
+
@count = 0
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class ForCollectionTag < LoopConditionTag
|
170
|
+
STATES = LoopConditionTag::STATES.merge \
|
171
|
+
0b0001 => 'loop always iterates more than once and always terminates early.',
|
172
|
+
0b0010 => 'loop always iterates only once and always terminates early.',
|
173
|
+
0b0011 => 'loop always terminates early',
|
174
|
+
0b0100 => 'loop always passes through'
|
175
|
+
|
176
|
+
def ping(value)
|
177
|
+
case value
|
178
|
+
when 't'
|
179
|
+
# start of iteration
|
180
|
+
@count += 1
|
181
|
+
when '@'
|
182
|
+
# end of iteration
|
183
|
+
@ends = true
|
184
|
+
when 'f'
|
185
|
+
# loop exit
|
186
|
+
case @count
|
187
|
+
when 0; @pass = true
|
188
|
+
when 1; @once = true
|
189
|
+
else; @twice = true
|
190
|
+
end
|
191
|
+
@count = 0
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class BranchConditionTag < Tag
|
197
|
+
attr_reader :true, :false
|
198
|
+
|
199
|
+
def initialize(*args)
|
200
|
+
clear
|
201
|
+
super
|
202
|
+
end
|
203
|
+
|
204
|
+
def type
|
205
|
+
:branch
|
206
|
+
end
|
207
|
+
|
208
|
+
def ping(value)
|
209
|
+
case value
|
210
|
+
when 't'; @true = true
|
211
|
+
when 'f'; @false = true
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def style
|
216
|
+
"b#{@true ? 1 : 0}#{@false ? 1 : 0 }"
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_f
|
220
|
+
(@true and @false) ? 100.0 : (@true or @false) ? 50.0 : 0.0
|
221
|
+
end
|
222
|
+
|
223
|
+
def complete?
|
224
|
+
@true and @false
|
225
|
+
end
|
226
|
+
|
227
|
+
def description
|
228
|
+
if @true and @false
|
229
|
+
'full coverage'
|
230
|
+
elsif @true
|
231
|
+
'never evaluates false'
|
232
|
+
elsif @false
|
233
|
+
'never evaluates true'
|
234
|
+
else
|
235
|
+
'never evaluated'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def clear
|
240
|
+
@true, @false = false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Piggly
|
2
|
+
|
3
|
+
#
|
4
|
+
# Walks the parse tree, attaching Tag values and rewriting source code to ping them.
|
5
|
+
#
|
6
|
+
class TraceCompiler
|
7
|
+
include FileCache
|
8
|
+
include CompilerCache
|
9
|
+
|
10
|
+
attr_accessor :nodes
|
11
|
+
|
12
|
+
def self.compile(tree, args)
|
13
|
+
new(args.fetch(:path)).send(:compile, tree)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.compiler_path
|
17
|
+
__FILE__
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(path)
|
21
|
+
# create unique prefix for each file, prepended to each node's tag
|
22
|
+
@prefix = File.expand_path(path)
|
23
|
+
@tags = []
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Destructively modifies +tree+ (by attaching tags) and returns the tree
|
28
|
+
# along with the modified source code, and the list of tags. The tag list
|
29
|
+
# is passed along to Profile to compute coverage information. The tree is
|
30
|
+
# passed to PrettyCompiler
|
31
|
+
#
|
32
|
+
def compile(tree)
|
33
|
+
puts "Compiling #{@prefix}"
|
34
|
+
return 'code.sql' => traverse(tree),
|
35
|
+
'tree' => tree,
|
36
|
+
'tags' => @tags,
|
37
|
+
'prefix' => @prefix
|
38
|
+
end
|
39
|
+
|
40
|
+
def traverse(node)
|
41
|
+
if node.terminal? or node.expression?
|
42
|
+
node.source_text
|
43
|
+
else
|
44
|
+
if node.respond_to?(:condStub) and node.respond_to?(:cond)
|
45
|
+
# preserve opening parenthesis and whitespace before injecting code. this way
|
46
|
+
# IF(test) becomes IF(piggly_cond(TAG, test)) instead of IFpiggly_cond(TAG, (test))
|
47
|
+
pre, cond = node.cond.expr.text_value.match(/\A(\(?[\t\n\r ]*)(.+)\z/m).captures
|
48
|
+
node.cond.source_text = ""
|
49
|
+
|
50
|
+
@tags << node.cond.tag(@prefix)
|
51
|
+
|
52
|
+
node.condStub.source_text = "#{pre}piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, #{cond})"
|
53
|
+
node.condStub.source_text << traverse(node.cond.tail) # preserve trailing whitespace
|
54
|
+
end
|
55
|
+
|
56
|
+
if node.respond_to?(:bodyStub)
|
57
|
+
if node.respond_to?(:exitStub) and node.respond_to?(:cond)
|
58
|
+
@tags << node.body.tag(@prefix)
|
59
|
+
@tags << node.cond.tag(@prefix)
|
60
|
+
|
61
|
+
# a hack to simulate a loop conditional statement in ForLoop. signal condition was true
|
62
|
+
# when body is executed. when exit stub is reached, signal condition was false
|
63
|
+
node.bodyStub.source_text = "perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, true);#{node.indent(:bodySpace)}"
|
64
|
+
node.bodyStub.source_text << "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
|
65
|
+
|
66
|
+
if node.respond_to?(:doneStub)
|
67
|
+
# signal the end of an iteration was reached
|
68
|
+
node.doneStub.source_text = "#{node.indent(:bodySpace)}perform piggly_signal($PIGGLY$#{node.cond.tag_id}$PIGGLY$, $PIGGLY$@$PIGGLY$);"
|
69
|
+
node.doneStub.source_text << node.body.indent
|
70
|
+
end
|
71
|
+
|
72
|
+
# signal the loop terminated
|
73
|
+
node.exitStub.source_text = "\n#{node.indent}perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, false);"
|
74
|
+
elsif node.respond_to?(:body)
|
75
|
+
# no condition:
|
76
|
+
# BEGIN ... END;
|
77
|
+
# LOOP ... END;
|
78
|
+
# ... ELSE ... END;
|
79
|
+
# CONTINUE label;
|
80
|
+
# EXIT label;
|
81
|
+
@tags << node.body.tag(@prefix)
|
82
|
+
node.bodyStub.source_text = "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# traverse children (in which we just injected code)
|
87
|
+
node.elements.map{|e| traverse(e) }.join
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[compiler cache])
|
2
|
+
require File.join(File.dirname(__FILE__), *%w[compiler tags])
|
3
|
+
require File.join(File.dirname(__FILE__), *%w[compiler trace])
|
4
|
+
require File.join(File.dirname(__FILE__), *%w[compiler pretty])
|
5
|
+
require File.join(File.dirname(__FILE__), *%w[compiler queue])
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Piggly
|
2
|
+
class Config
|
3
|
+
|
4
|
+
def self.config_accessor(hash)
|
5
|
+
hash.keys.each do |name|
|
6
|
+
self.class.send(:define_method, name) do
|
7
|
+
instance_variable_get("@#{name}") || hash[name]
|
8
|
+
end
|
9
|
+
self.class.send(:define_method, "#{name}=") do |value|
|
10
|
+
instance_variable_set("@#{name}", value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
config_accessor :cache_root => File.expand_path(File.join(Dir.pwd, 'piggly', 'cache')),
|
16
|
+
:report_root => File.expand_path(File.join(Dir.pwd, 'piggly', 'reports')),
|
17
|
+
:piggly_root => PIGGLY_ROOT,
|
18
|
+
:trace_prefix => 'PIGGLY',
|
19
|
+
:aggregate => false
|
20
|
+
|
21
|
+
def self.path(root, file=nil)
|
22
|
+
if file
|
23
|
+
file[%r{^\.\.|^\/|^(?:[A-Z]:)?/}i] ?
|
24
|
+
file : # ../path, /path, or D:\path that isn't relative to root
|
25
|
+
File.join(root, file)
|
26
|
+
else
|
27
|
+
root
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.mkpath(root, file=nil)
|
32
|
+
if file.nil?
|
33
|
+
FileUtils.makedirs(root)
|
34
|
+
root
|
35
|
+
else
|
36
|
+
path = path(root, file)
|
37
|
+
FileUtils.makedirs(File.dirname(path))
|
38
|
+
path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class File
|
2
|
+
|
3
|
+
# True if target file is older (by mtime) than any source file
|
4
|
+
def self.stale?(target, *sources)
|
5
|
+
if exists?(target)
|
6
|
+
oldest = mtime(target)
|
7
|
+
sources.any?{|x| mtime(x) > oldest }
|
8
|
+
else
|
9
|
+
true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
module Piggly
|
16
|
+
module FileCache
|
17
|
+
def self.included(subclass)
|
18
|
+
subclass.extend(ClassMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# Maps source path to cache path, like /home/user/foo.sql => piggly/cache/#{MD5('/home/user')}/#{BaseClass}/foo.sql
|
24
|
+
def cache_path(file)
|
25
|
+
# up to the last capitalized word of the class name
|
26
|
+
subdir = name[/^(?:.+::)?(.+?)([A-Z][^A-Z]+)?$/, 1]
|
27
|
+
root = File.join(Config.cache_root, subdir)
|
28
|
+
|
29
|
+
# md5 the full path to prevent collisions
|
30
|
+
full = File.expand_path(file)
|
31
|
+
base = File.basename(full)
|
32
|
+
hash = Digest::MD5.hexdigest(File.dirname(full))
|
33
|
+
|
34
|
+
Config.mkpath(File.join(Config.cache_root, hash, subdir), base)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Piggly
|
2
|
+
class Installer
|
3
|
+
|
4
|
+
# Compiles the procedures in +file+ with instrumentation and installs them
|
5
|
+
def self.trace_proc(file)
|
6
|
+
# recompile with instrumentation if needed
|
7
|
+
cache = Piggly::TraceCompiler.cache(file)
|
8
|
+
|
9
|
+
# install instrumented code
|
10
|
+
connection.exec cache['code.sql']
|
11
|
+
|
12
|
+
# map tag messages to tag objects
|
13
|
+
Profile.add(file, cache['tags'], cache)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reinstalls the original stored procedures in +file+
|
17
|
+
def self.untrace_proc(file)
|
18
|
+
connection.exec File.read(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Installs necessary instrumentation support
|
22
|
+
def self.install_trace
|
23
|
+
# record trace messages
|
24
|
+
connection.set_notice_processor(&Profile.notice_processor)
|
25
|
+
|
26
|
+
# install tracing functions
|
27
|
+
connection.exec <<-SQL
|
28
|
+
-- signals that a conditional expression was executed
|
29
|
+
CREATE OR REPLACE FUNCTION piggly_cond(message varchar, value boolean)
|
30
|
+
RETURNS boolean AS $$
|
31
|
+
BEGIN
|
32
|
+
IF value THEN
|
33
|
+
RAISE WARNING '#{Config.trace_prefix} % t', message;
|
34
|
+
ELSE
|
35
|
+
RAISE WARNING '#{Config.trace_prefix} % f', message;
|
36
|
+
END IF;
|
37
|
+
RETURN value;
|
38
|
+
END $$ LANGUAGE 'plpgsql' VOLATILE;
|
39
|
+
SQL
|
40
|
+
|
41
|
+
connection.exec <<-SQL
|
42
|
+
-- generic signal
|
43
|
+
CREATE OR REPLACE FUNCTION piggly_signal(message varchar, signal varchar)
|
44
|
+
RETURNS void AS $$
|
45
|
+
BEGIN
|
46
|
+
RAISE WARNING '#{Config.trace_prefix} % %', message, signal;
|
47
|
+
END $$ LANGUAGE 'plpgsql' VOLATILE;
|
48
|
+
SQL
|
49
|
+
|
50
|
+
connection.exec <<-SQL
|
51
|
+
-- signals that a (sub)expression was executed. handles '' and NULL value
|
52
|
+
CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value varchar)
|
53
|
+
RETURNS varchar AS $$
|
54
|
+
BEGIN
|
55
|
+
RAISE WARNING '#{Config.trace_prefix} %', message;
|
56
|
+
RETURN value;
|
57
|
+
END $$ LANGUAGE 'plpgsql' VOLATILE;
|
58
|
+
SQL
|
59
|
+
|
60
|
+
connection.exec <<-SQL
|
61
|
+
-- signals that a (sub)expression was executed. handles all other types
|
62
|
+
CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value anyelement)
|
63
|
+
RETURNS anyelement AS $$
|
64
|
+
BEGIN
|
65
|
+
RAISE WARNING '#{Config.trace_prefix} %', message;
|
66
|
+
RETURN value;
|
67
|
+
END $$ LANGUAGE 'plpgsql' VOLATILE;
|
68
|
+
SQL
|
69
|
+
|
70
|
+
connection.exec <<-SQL
|
71
|
+
-- signals that a branch was taken
|
72
|
+
CREATE OR REPLACE FUNCTION piggly_branch(message varchar)
|
73
|
+
RETURNS void AS $$
|
74
|
+
BEGIN
|
75
|
+
RAISE WARNING '#{Config.trace_prefix} %', message;
|
76
|
+
END $$ LANGUAGE 'plpgsql' VOLATILE;
|
77
|
+
SQL
|
78
|
+
end
|
79
|
+
|
80
|
+
# Uninstalls instrumentation support
|
81
|
+
def self.uninstall_trace
|
82
|
+
connection.set_notice_processor
|
83
|
+
connection.exec "DROP FUNCTION IF EXISTS piggly_cond(varchar, boolean);"
|
84
|
+
connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, varchar);"
|
85
|
+
connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, anyelement);"
|
86
|
+
connection.exec "DROP FUNCTION IF EXISTS piggly_branch(varchar);"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the active PGConn
|
90
|
+
def self.connection
|
91
|
+
ActiveRecord::Base.connection.raw_connection
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|