piggly-nsd 2.3.3
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.
- checksums.yaml +7 -0
- data/README.md +170 -0
- data/Rakefile +33 -0
- data/bin/piggly +8 -0
- data/lib/piggly/command/base.rb +148 -0
- data/lib/piggly/command/report.rb +162 -0
- data/lib/piggly/command/trace.rb +90 -0
- data/lib/piggly/command/untrace.rb +78 -0
- data/lib/piggly/command.rb +8 -0
- data/lib/piggly/compiler/cache_dir.rb +119 -0
- data/lib/piggly/compiler/coverage_report.rb +63 -0
- data/lib/piggly/compiler/trace_compiler.rb +117 -0
- data/lib/piggly/compiler.rb +7 -0
- data/lib/piggly/config.rb +80 -0
- data/lib/piggly/dumper/index.rb +121 -0
- data/lib/piggly/dumper/qualified_name.rb +36 -0
- data/lib/piggly/dumper/qualified_type.rb +141 -0
- data/lib/piggly/dumper/reified_procedure.rb +172 -0
- data/lib/piggly/dumper/skeleton_procedure.rb +112 -0
- data/lib/piggly/dumper.rb +9 -0
- data/lib/piggly/installer.rb +137 -0
- data/lib/piggly/parser/grammar.tt +748 -0
- data/lib/piggly/parser/nodes.rb +378 -0
- data/lib/piggly/parser/traversal.rb +50 -0
- data/lib/piggly/parser/treetop_ruby19_patch.rb +21 -0
- data/lib/piggly/parser.rb +69 -0
- data/lib/piggly/profile.rb +108 -0
- data/lib/piggly/reporter/base.rb +106 -0
- data/lib/piggly/reporter/html_dsl.rb +63 -0
- data/lib/piggly/reporter/index.rb +114 -0
- data/lib/piggly/reporter/procedure.rb +129 -0
- data/lib/piggly/reporter/resources/highlight.js +38 -0
- data/lib/piggly/reporter/resources/piggly.css +515 -0
- data/lib/piggly/reporter/resources/sortable.js +493 -0
- data/lib/piggly/reporter.rb +8 -0
- data/lib/piggly/tags.rb +280 -0
- data/lib/piggly/task.rb +215 -0
- data/lib/piggly/util/blankslate.rb +114 -0
- data/lib/piggly/util/cacheable.rb +19 -0
- data/lib/piggly/util/enumerable.rb +44 -0
- data/lib/piggly/util/file.rb +17 -0
- data/lib/piggly/util/process_queue.rb +96 -0
- data/lib/piggly/util/thunk.rb +39 -0
- data/lib/piggly/util.rb +9 -0
- data/lib/piggly/version.rb +15 -0
- data/lib/piggly.rb +20 -0
- data/spec/examples/compiler/cacheable_spec.rb +190 -0
- data/spec/examples/compiler/report_spec.rb +25 -0
- data/spec/examples/compiler/trace_spec.rb +123 -0
- data/spec/examples/config_spec.rb +63 -0
- data/spec/examples/dumper/index_spec.rb +199 -0
- data/spec/examples/dumper/procedure_spec.rb +116 -0
- data/spec/examples/grammar/expression_spec.rb +302 -0
- data/spec/examples/grammar/statements/assignment_spec.rb +70 -0
- data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
- data/spec/examples/grammar/statements/exception_spec.rb +78 -0
- data/spec/examples/grammar/statements/if_spec.rb +191 -0
- data/spec/examples/grammar/statements/loop_spec.rb +41 -0
- data/spec/examples/grammar/statements/sql_spec.rb +71 -0
- data/spec/examples/grammar/tokens/comment_spec.rb +58 -0
- data/spec/examples/grammar/tokens/datatype_spec.rb +58 -0
- data/spec/examples/grammar/tokens/identifier_spec.rb +74 -0
- data/spec/examples/grammar/tokens/keyword_spec.rb +44 -0
- data/spec/examples/grammar/tokens/label_spec.rb +40 -0
- data/spec/examples/grammar/tokens/literal_spec.rb +30 -0
- data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
- data/spec/examples/grammar/tokens/number_spec.rb +34 -0
- data/spec/examples/grammar/tokens/sqlkeywords_spec.rb +45 -0
- data/spec/examples/grammar/tokens/string_spec.rb +54 -0
- data/spec/examples/grammar/tokens/whitespace_spec.rb +40 -0
- data/spec/examples/installer_spec.rb +59 -0
- data/spec/examples/parser/nodes_spec.rb +73 -0
- data/spec/examples/parser/traversal_spec.rb +14 -0
- data/spec/examples/parser_spec.rb +118 -0
- data/spec/examples/profile_spec.rb +153 -0
- data/spec/examples/reporter/html/dsl_spec.rb +0 -0
- data/spec/examples/reporter/html/index_spec.rb +0 -0
- data/spec/examples/reporter/html_spec.rb +1 -0
- data/spec/examples/reporter_spec.rb +0 -0
- data/spec/examples/tags_spec.rb +285 -0
- data/spec/examples/task_spec.rb +0 -0
- data/spec/examples/util/cacheable_spec.rb +41 -0
- data/spec/examples/util/enumerable_spec.rb +64 -0
- data/spec/examples/util/file_spec.rb +40 -0
- data/spec/examples/util/process_queue_spec.rb +16 -0
- data/spec/examples/util/thunk_spec.rb +59 -0
- data/spec/examples/version_spec.rb +0 -0
- data/spec/issues/007_spec.rb +25 -0
- data/spec/issues/008_spec.rb +73 -0
- data/spec/issues/018_spec.rb +25 -0
- data/spec/issues/028_spec.rb +48 -0
- data/spec/issues/032_spec.rb +98 -0
- data/spec/issues/036_spec.rb +41 -0
- data/spec/spec_helper.rb +312 -0
- data/spec/spec_suite.rb +5 -0
- metadata +162 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module Piggly
|
|
4
|
+
module Compiler
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# Each cache unit (any group of data that should be expired and created
|
|
8
|
+
# together) can be broken apart, to prevent unmarshaling a huge block of
|
|
9
|
+
# data all at once.
|
|
10
|
+
#
|
|
11
|
+
# The interface works like a Hash, so the compile method should return a
|
|
12
|
+
# hash of objects. Each object is writen to a different file (named by the
|
|
13
|
+
# hash key) within the same directory. String objects are (usually) read
|
|
14
|
+
# and written directly to disk, while all other objects are (un-)Marshal'd
|
|
15
|
+
#
|
|
16
|
+
# Cache invalidation is done by comparing mtime timestamps on the cached
|
|
17
|
+
# object's file to all the "source" files (ruby libs, input files, etc)
|
|
18
|
+
# required to regenerate the data.
|
|
19
|
+
#
|
|
20
|
+
class CacheDir
|
|
21
|
+
# Non-printable ASCII char indicates data should be Marshal'd
|
|
22
|
+
HINT = /[\u{0000}-\u{0008}\u{000E}-\u{001F}\u{007F}-\u{00C0}]/
|
|
23
|
+
|
|
24
|
+
def initialize(dir)
|
|
25
|
+
@dir = dir
|
|
26
|
+
@data = Hash.new do |h, k|
|
|
27
|
+
path = File.join(@dir, k.to_s)
|
|
28
|
+
if File.exist?(path)
|
|
29
|
+
h[k.to_s] = File.open(path, "rb:UTF-8") do |io|
|
|
30
|
+
# Detect Marshal'd data
|
|
31
|
+
if io.read(2) !~ HINT
|
|
32
|
+
io.rewind
|
|
33
|
+
io.read
|
|
34
|
+
else
|
|
35
|
+
io.rewind
|
|
36
|
+
Marshal.load(io)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load given key from file system into memory if needed
|
|
44
|
+
# @return [Object]
|
|
45
|
+
def [](key)
|
|
46
|
+
@data[key.to_s]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Writes through to file system
|
|
50
|
+
# @return [void]
|
|
51
|
+
def []=(key, value)
|
|
52
|
+
@data[key.to_s] = value
|
|
53
|
+
write(key.to_s => value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Writes through to file system and returns self
|
|
57
|
+
# @return [CacheDir] self
|
|
58
|
+
def update(hash)
|
|
59
|
+
hash.each{|k,v| self[k] = v }
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [void]
|
|
64
|
+
def delete(key)
|
|
65
|
+
path = File.join(@dir, key.to_s)
|
|
66
|
+
File.unlink(path) if File.exist?(path)
|
|
67
|
+
@data.delete(key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<String>]
|
|
71
|
+
def keys
|
|
72
|
+
Dir[@dir + "/*"].map{|f| File.basename(f) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Creates cachedir, destroys its contents, and returns self
|
|
76
|
+
# @return [CacheDir] self
|
|
77
|
+
def clear
|
|
78
|
+
@data.clear
|
|
79
|
+
|
|
80
|
+
if File.exist?(@dir)
|
|
81
|
+
FileUtils.rm(Dir["#{@dir}/*"])
|
|
82
|
+
FileUtils.touch(@dir)
|
|
83
|
+
else
|
|
84
|
+
FileUtils.mkdir(@dir)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Clears entire cache, replaces contents, and returns self
|
|
91
|
+
# @return [CacheDir] self
|
|
92
|
+
def replace(hash)
|
|
93
|
+
clear
|
|
94
|
+
update(hash)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Serializes each entry to disk
|
|
100
|
+
# @return [void]
|
|
101
|
+
def write(hash)
|
|
102
|
+
FileUtils.mkdir(@dir) unless File.exist?(@dir)
|
|
103
|
+
FileUtils.touch(@dir) # Update mtime
|
|
104
|
+
|
|
105
|
+
hash.each do |key, value|
|
|
106
|
+
File.open(File.join(@dir, key.to_s), "wb:UTF-8") do |io|
|
|
107
|
+
# Marshal if the first two bytes contain non-ASCII
|
|
108
|
+
if value.is_a?(String) and value[0,2] !~ HINT
|
|
109
|
+
io.write(value)
|
|
110
|
+
else
|
|
111
|
+
Marshal.dump(value, io)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Compiler
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# Produces HTML output to report coverage of tagged nodes in the tree
|
|
6
|
+
#
|
|
7
|
+
class CoverageReport
|
|
8
|
+
include Reporter::HtmlDsl
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile(procedure, profile)
|
|
15
|
+
trace = Compiler::TraceCompiler.new(@config)
|
|
16
|
+
|
|
17
|
+
if trace.stale?(procedure)
|
|
18
|
+
raise StaleCacheError,
|
|
19
|
+
"stale cached syntax tree for #{procedure.name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get (copies of) the tagged nodes from the compiled tree
|
|
23
|
+
data = trace.compile(procedure)
|
|
24
|
+
|
|
25
|
+
return :html => traverse(data[:tree], profile),
|
|
26
|
+
:lines => 1 .. procedure.source(@config).count("\n") + 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
# @return [String]
|
|
32
|
+
def traverse(node, profile, string = "")
|
|
33
|
+
if node.tagged?
|
|
34
|
+
tag = profile[node.tag_id]
|
|
35
|
+
|
|
36
|
+
if tag.complete?
|
|
37
|
+
string << %[<span class="#{tag.style}" id="T#{tag.id}">]
|
|
38
|
+
else
|
|
39
|
+
string << %[<span class="#{tag.style}" id="T#{tag.id}" title="#{tag.description}">]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if node.terminal?
|
|
44
|
+
if style = node.style
|
|
45
|
+
string << %[<span class="#{style}">#{e(node.text_value)}</span>]
|
|
46
|
+
else
|
|
47
|
+
string << e(node.text_value)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
# Non-terminals never write their text_value
|
|
51
|
+
node.elements.each{|child| traverse(child, profile, string) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if node.tagged?
|
|
55
|
+
string << %[</span>]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
string
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Compiler
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# Walks the parse tree, attaching Tag values and rewriting source code to ping them.
|
|
6
|
+
#
|
|
7
|
+
class TraceCompiler
|
|
8
|
+
include Util::Cacheable
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Is the cache_path is older than its source path or the other files?
|
|
15
|
+
def stale?(procedure)
|
|
16
|
+
Util::File.stale?(cache_path(procedure.source_path(@config)),
|
|
17
|
+
procedure.source_path(@config),
|
|
18
|
+
*self.class.cache_sources)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def compile(procedure)
|
|
22
|
+
cache = CacheDir.new(cache_path(procedure.source_path(@config)))
|
|
23
|
+
|
|
24
|
+
if stale?(procedure)
|
|
25
|
+
begin
|
|
26
|
+
$stdout.puts "Compiling #{procedure.name}"
|
|
27
|
+
tree = Parser.parse(IO.read(procedure.source_path(@config)))
|
|
28
|
+
tree = tree.force! if tree.respond_to?(:thunk?)
|
|
29
|
+
|
|
30
|
+
tags = []
|
|
31
|
+
code = traverse(tree, procedure.oid, tags)
|
|
32
|
+
|
|
33
|
+
cache.replace(:tree => tree, :code => code, :tags => tags)
|
|
34
|
+
rescue RuntimeError => e
|
|
35
|
+
$stdout.puts <<-EXMSG
|
|
36
|
+
****
|
|
37
|
+
Error compiling procedure #{procedure.name}
|
|
38
|
+
Source: #{procedure.source_path(@config)}
|
|
39
|
+
Exception Message:
|
|
40
|
+
#{e.message}
|
|
41
|
+
****
|
|
42
|
+
EXMSG
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
cache
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
# Rewrites the parse tree to call instrumentation helpers, and destructively
|
|
53
|
+
# updates `tags` by appending the tags of instrumented nodes
|
|
54
|
+
# @return [String]
|
|
55
|
+
def traverse(node, oid, tags)
|
|
56
|
+
if node.terminal? or node.expression?
|
|
57
|
+
node.source_text
|
|
58
|
+
else
|
|
59
|
+
if node.respond_to?(:condStub) and node.respond_to?(:cond)
|
|
60
|
+
# Preserve opening parenthesis and whitespace before injecting code. This way
|
|
61
|
+
# IF(test) becomes IF(piggly_cond(TAG, test)) instead of IFpiggly_cond(TAG, (test))
|
|
62
|
+
pre, cond = node.cond.expr.text_value.match(/\A(\(?[\t\n\r ]*)(.+)\z/m).captures
|
|
63
|
+
node.cond.source_text = ""
|
|
64
|
+
|
|
65
|
+
tags << node.cond.tag(oid)
|
|
66
|
+
|
|
67
|
+
node.condStub.source_text = "#{pre}public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, (#{cond}))"
|
|
68
|
+
node.condStub.source_text << traverse(node.cond.tail, oid, tags) # preserve trailing whitespace
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if node.respond_to?(:bodyStub)
|
|
72
|
+
if node.respond_to?(:exitStub) and node.respond_to?(:cond)
|
|
73
|
+
tags << node.body.tag(oid)
|
|
74
|
+
tags << node.cond.tag(oid)
|
|
75
|
+
|
|
76
|
+
# Hack to simulate a loop conditional statement in stmtForLoop and stmtLoop.
|
|
77
|
+
# signal condition is true when body is executed, and false when exit stub is reached
|
|
78
|
+
node.bodyStub.source_text = "perform public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, true);#{node.indent(:bodySpace)}"
|
|
79
|
+
node.bodyStub.source_text << "perform public.piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
|
|
80
|
+
|
|
81
|
+
if node.respond_to?(:doneStub)
|
|
82
|
+
# Signal the end of an iteration was reached
|
|
83
|
+
node.doneStub.source_text = "#{node.indent(:bodySpace)}perform public.piggly_signal($PIGGLY$#{node.cond.tag_id}$PIGGLY$, $PIGGLY$@$PIGGLY$);"
|
|
84
|
+
node.doneStub.source_text << node.body.indent
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Signal the loop terminated
|
|
88
|
+
node.exitStub.source_text = "\n#{node.indent}perform public.piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, false);"
|
|
89
|
+
elsif node.respond_to?(:body)
|
|
90
|
+
# Unconditional branches (or blocks)
|
|
91
|
+
# BEGIN ... END;
|
|
92
|
+
# ... ELSE ... END;
|
|
93
|
+
# CONTINUE label;
|
|
94
|
+
# EXIT label;
|
|
95
|
+
tags << node.body.tag(oid)
|
|
96
|
+
node.bodyStub.source_text = "perform public.piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Traverse children (in which we just injected code)
|
|
101
|
+
node.elements.map{|e| traverse(e, oid, tags) }.join
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class << TraceCompiler
|
|
107
|
+
|
|
108
|
+
# Each of these files' mtimes are used to determine when another file is stale
|
|
109
|
+
def cache_sources
|
|
110
|
+
[Parser.grammar_path,
|
|
111
|
+
Parser.parser_path,
|
|
112
|
+
Parser.nodes_path]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
class Config
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class << Config
|
|
6
|
+
def path(root, file=nil)
|
|
7
|
+
if file.nil?
|
|
8
|
+
root
|
|
9
|
+
else
|
|
10
|
+
file[%r{^\.\.|^\/|^(?:[A-Z]:)?/}i] ?
|
|
11
|
+
file : # ../path, /path, or D:\path that isn't relative to root
|
|
12
|
+
File.join(root, file)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mkpath(root, file=nil)
|
|
17
|
+
if file.nil?
|
|
18
|
+
FileUtils.makedirs(root)
|
|
19
|
+
root
|
|
20
|
+
else
|
|
21
|
+
path = path(root, file)
|
|
22
|
+
FileUtils.makedirs(File.dirname(path))
|
|
23
|
+
path
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def config_accessor(hash)
|
|
30
|
+
hash = hash.clone
|
|
31
|
+
hash.keys.each do |name|
|
|
32
|
+
define_method(name) do
|
|
33
|
+
instance_variable_get("@#{name}") || hash[name]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
define_method("#{name}?") do
|
|
37
|
+
instance_variable_get("@#{name}") || hash[name]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
define_method("#{name}=") do |value|
|
|
41
|
+
instance_variable_set("@#{name}", value)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Config
|
|
48
|
+
attr_accessor \
|
|
49
|
+
:cache_root,
|
|
50
|
+
:report_root,
|
|
51
|
+
:database_yml,
|
|
52
|
+
:connection_name,
|
|
53
|
+
:trace_prefix,
|
|
54
|
+
:accumulate,
|
|
55
|
+
:dry_run,
|
|
56
|
+
:filters
|
|
57
|
+
|
|
58
|
+
alias accumulate? accumulate
|
|
59
|
+
alias dry_run? dry_run
|
|
60
|
+
|
|
61
|
+
def path(*args)
|
|
62
|
+
self.class.path(*args)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def mkpath(*args)
|
|
66
|
+
self.class.mkpath(*args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize
|
|
70
|
+
@cache_root = File.expand_path("#{Dir.pwd}/piggly/cache")
|
|
71
|
+
@report_root = File.expand_path("#{Dir.pwd}/piggly/reports")
|
|
72
|
+
@database_yml = nil
|
|
73
|
+
@connection_name = "piggly"
|
|
74
|
+
@trace_prefix = "PIGGLY"
|
|
75
|
+
@accumulate = false
|
|
76
|
+
@dry_run = false
|
|
77
|
+
@filters = []
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Dumper
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# The index file stores metadata about every procedure, but the source
|
|
6
|
+
# code is stored in a separate file for each procedure.
|
|
7
|
+
#
|
|
8
|
+
class Index
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [String]
|
|
15
|
+
def path
|
|
16
|
+
@config.mkpath("#{@config.cache_root}/Dumper", "index.yml")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Updates the index with the given list of Procedure values
|
|
20
|
+
# @return [void]
|
|
21
|
+
def update(procedures)
|
|
22
|
+
newest = Util::Enumerable.index_by(procedures){|x| x.identifier }
|
|
23
|
+
|
|
24
|
+
removed = index.values.reject{|p| newest.include?(p.identifier) }
|
|
25
|
+
removed.each{|p| p.purge_source(@config) }
|
|
26
|
+
|
|
27
|
+
added = procedures.reject{|p| index.include?(p.identifier) }
|
|
28
|
+
added.each{|p| p.store_source(@config) }
|
|
29
|
+
|
|
30
|
+
changed = procedures.select do |p|
|
|
31
|
+
if mine = index[p.identifier]
|
|
32
|
+
# If both are skeletons, they will have the same source because they
|
|
33
|
+
# are read from the same file, so don't bother checking that case
|
|
34
|
+
not (mine.skeleton? and p.skeleton?) and
|
|
35
|
+
mine.source(@config) != p.source(@config)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
changed.each{|p| p.store_source(@config) }
|
|
39
|
+
|
|
40
|
+
@index = newest
|
|
41
|
+
store_index
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a list of Procedure values from the index
|
|
45
|
+
def procedures
|
|
46
|
+
index.values
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the Procedure with the given identifier
|
|
50
|
+
def [](identifier)
|
|
51
|
+
p = index[identifier]
|
|
52
|
+
p.dup if p
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the shortest human-readable label that distinctly identifies
|
|
56
|
+
# the given procedure from the other procedures in the index
|
|
57
|
+
def label(procedure)
|
|
58
|
+
others =
|
|
59
|
+
procedures.reject{|p| p.oid == procedure.oid }
|
|
60
|
+
|
|
61
|
+
same =
|
|
62
|
+
others.all?{|p| p.name.schema == procedure.name.schema }
|
|
63
|
+
|
|
64
|
+
name =
|
|
65
|
+
if same
|
|
66
|
+
procedure.name.name
|
|
67
|
+
else
|
|
68
|
+
procedure.name.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
samenames =
|
|
72
|
+
others.select{|p| p.name == procedure.name }
|
|
73
|
+
|
|
74
|
+
if samenames.empty?
|
|
75
|
+
# Name is unique enough
|
|
76
|
+
name.to_s
|
|
77
|
+
else
|
|
78
|
+
sameargs =
|
|
79
|
+
samenames.select{|p| p.arg_types == procedure.arg_types }
|
|
80
|
+
|
|
81
|
+
if sameargs.empty?
|
|
82
|
+
# Name and arg types are unique enough
|
|
83
|
+
"#{name}(#{procedure.arg_types.join(", ")})"
|
|
84
|
+
else
|
|
85
|
+
samemodes =
|
|
86
|
+
sameargs.select{|p| p.arg_modes == procedure.arg_modes }
|
|
87
|
+
|
|
88
|
+
if samemodes.empty?
|
|
89
|
+
# Name, arg types, and arg modes are unique enough
|
|
90
|
+
"#{name}(#{procedure.arg_modes.zip(procedure.arg_types).map{|a,b| "#{a} #{b}" }.join(", ")})"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def index
|
|
99
|
+
@index ||= load_index
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Load the index from disk
|
|
103
|
+
def load_index
|
|
104
|
+
contents =
|
|
105
|
+
unless File.exist?(path)
|
|
106
|
+
[]
|
|
107
|
+
else
|
|
108
|
+
YAML.unsafe_load(File.read(path, encoding: 'UTF-8'))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Util::Enumerable.index_by(contents){|x| x.identifier }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Write the index to disk
|
|
115
|
+
def store_index
|
|
116
|
+
File.open(path, "wb:UTF-8"){|io| YAML.dump(procedures.map{|p| p.skeleton }, io) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Dumper
|
|
3
|
+
|
|
4
|
+
class QualifiedName
|
|
5
|
+
attr_reader :schema, :name
|
|
6
|
+
|
|
7
|
+
def initialize(schema, name)
|
|
8
|
+
@schema, @name = schema, name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [String]
|
|
12
|
+
def quote
|
|
13
|
+
if @schema
|
|
14
|
+
'"' + @schema + '"."' + @name + '"'
|
|
15
|
+
else
|
|
16
|
+
'"' + @name + '"'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
def to_s
|
|
22
|
+
if @schema
|
|
23
|
+
@schema + "." + @name
|
|
24
|
+
else
|
|
25
|
+
@name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def ==(other)
|
|
31
|
+
self.to_s == other.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Dumper
|
|
3
|
+
|
|
4
|
+
# used for RETURN TABLE(...)
|
|
5
|
+
class RecordType
|
|
6
|
+
attr_reader :types, :names, :modes, :defaults
|
|
7
|
+
|
|
8
|
+
def initialize(types, names, modes, defaults)
|
|
9
|
+
@types, @names, @modes, @defaults =
|
|
10
|
+
types, names, modes, defaults
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def quote
|
|
14
|
+
"table (#{@types.zip(@names, @modes, @defaults).map do |type, name, mode, default|
|
|
15
|
+
"#{name.quote + " " if name}#{type.quote}#{" default " + default if default}"
|
|
16
|
+
end.join(", ")})"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def table?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class QualifiedType
|
|
25
|
+
attr_reader :schema, :name
|
|
26
|
+
|
|
27
|
+
def self.parse(name, rest = nil)
|
|
28
|
+
if rest.to_s == ""
|
|
29
|
+
schema = nil
|
|
30
|
+
else
|
|
31
|
+
schema = unquote(name)
|
|
32
|
+
name = rest
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
case name
|
|
36
|
+
when /(.*)\[\]$/
|
|
37
|
+
name = $1
|
|
38
|
+
array = "[]"
|
|
39
|
+
else
|
|
40
|
+
array = ""
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if schema.to_s == ""
|
|
44
|
+
fst, snd = name.split(".", 2)
|
|
45
|
+
if snd.nil?
|
|
46
|
+
new(nil, unquote(fst), array)
|
|
47
|
+
else
|
|
48
|
+
new(unquote(fst), unquote(snd), array)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
new(schema, unquote(name), array)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.unquote(s)
|
|
56
|
+
return s if s.nil?
|
|
57
|
+
s[/^"(.*)"$/, 1] || s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(schema, name, array)
|
|
61
|
+
@schema, @name, @array = schema, name, array
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def table?
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shorten
|
|
69
|
+
self.class.new(nil, @name, @array)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def quote
|
|
73
|
+
if @schema
|
|
74
|
+
'"' + @schema + '"."' + normalize(@name) + '"' + @array
|
|
75
|
+
else
|
|
76
|
+
'"' + normalize(@name) + '"' + @array
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_s
|
|
81
|
+
unless [nil, "", "pg_catalog"].include?(@schema)
|
|
82
|
+
@schema + "." + readable(@name) + @array
|
|
83
|
+
else
|
|
84
|
+
readable(@name) + @array
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ==(other)
|
|
89
|
+
self.to_s == other.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
protected
|
|
93
|
+
|
|
94
|
+
def normalize(name)
|
|
95
|
+
unless [nil, "", "pg_catalog"].include?(@schema)
|
|
96
|
+
return name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# select format_type(ret.oid, null), ret.typname
|
|
100
|
+
# from pg_type as ret
|
|
101
|
+
# where ret.typname <> format_type(ret.oid, null)
|
|
102
|
+
# and ret.typname not like '\\_%'
|
|
103
|
+
# group by ret.typname, format_type(ret.oid, null)
|
|
104
|
+
# order by format_type(ret.oid, null);
|
|
105
|
+
case name
|
|
106
|
+
when '"any"' then "any"
|
|
107
|
+
when "bigint" then "int8"
|
|
108
|
+
when "bit varying" then "varbit"
|
|
109
|
+
when "boolean" then "bool"
|
|
110
|
+
when '"char"' then "char"
|
|
111
|
+
when "character" then "bpchar"
|
|
112
|
+
when "character varying" then "varchar"
|
|
113
|
+
when "double precision" then "float8"
|
|
114
|
+
when "information_schema\.(.*)" then $1
|
|
115
|
+
when "integer" then "int4"
|
|
116
|
+
when "real" then "float4"
|
|
117
|
+
when "smallint" then "int2"
|
|
118
|
+
when "timestamp without time zone" then "timestamp"
|
|
119
|
+
when "timestamp with time zone" then "timestamptz"
|
|
120
|
+
when "time without time zone" then "time"
|
|
121
|
+
when "time with time zone" then "timetz"
|
|
122
|
+
else name
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def readable(name)
|
|
127
|
+
case name
|
|
128
|
+
when /^_(.*)/ then "#{readable($1)}[]"
|
|
129
|
+
when "bpchar" then "char"
|
|
130
|
+
when /^float4(.*)/ then "real#{$1}"
|
|
131
|
+
when /^int2(.*)/ then "smallint#{$1}"
|
|
132
|
+
when /^int4(.*)/ then "int#{$1}"
|
|
133
|
+
when /^int8(.*)/ then "bigint#{$1}"
|
|
134
|
+
when /^serial4(.*)/ then "serial#{$1}"
|
|
135
|
+
else name
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
141
|
+
end
|