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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +170 -0
  3. data/Rakefile +33 -0
  4. data/bin/piggly +8 -0
  5. data/lib/piggly/command/base.rb +148 -0
  6. data/lib/piggly/command/report.rb +162 -0
  7. data/lib/piggly/command/trace.rb +90 -0
  8. data/lib/piggly/command/untrace.rb +78 -0
  9. data/lib/piggly/command.rb +8 -0
  10. data/lib/piggly/compiler/cache_dir.rb +119 -0
  11. data/lib/piggly/compiler/coverage_report.rb +63 -0
  12. data/lib/piggly/compiler/trace_compiler.rb +117 -0
  13. data/lib/piggly/compiler.rb +7 -0
  14. data/lib/piggly/config.rb +80 -0
  15. data/lib/piggly/dumper/index.rb +121 -0
  16. data/lib/piggly/dumper/qualified_name.rb +36 -0
  17. data/lib/piggly/dumper/qualified_type.rb +141 -0
  18. data/lib/piggly/dumper/reified_procedure.rb +172 -0
  19. data/lib/piggly/dumper/skeleton_procedure.rb +112 -0
  20. data/lib/piggly/dumper.rb +9 -0
  21. data/lib/piggly/installer.rb +137 -0
  22. data/lib/piggly/parser/grammar.tt +748 -0
  23. data/lib/piggly/parser/nodes.rb +378 -0
  24. data/lib/piggly/parser/traversal.rb +50 -0
  25. data/lib/piggly/parser/treetop_ruby19_patch.rb +21 -0
  26. data/lib/piggly/parser.rb +69 -0
  27. data/lib/piggly/profile.rb +108 -0
  28. data/lib/piggly/reporter/base.rb +106 -0
  29. data/lib/piggly/reporter/html_dsl.rb +63 -0
  30. data/lib/piggly/reporter/index.rb +114 -0
  31. data/lib/piggly/reporter/procedure.rb +129 -0
  32. data/lib/piggly/reporter/resources/highlight.js +38 -0
  33. data/lib/piggly/reporter/resources/piggly.css +515 -0
  34. data/lib/piggly/reporter/resources/sortable.js +493 -0
  35. data/lib/piggly/reporter.rb +8 -0
  36. data/lib/piggly/tags.rb +280 -0
  37. data/lib/piggly/task.rb +215 -0
  38. data/lib/piggly/util/blankslate.rb +114 -0
  39. data/lib/piggly/util/cacheable.rb +19 -0
  40. data/lib/piggly/util/enumerable.rb +44 -0
  41. data/lib/piggly/util/file.rb +17 -0
  42. data/lib/piggly/util/process_queue.rb +96 -0
  43. data/lib/piggly/util/thunk.rb +39 -0
  44. data/lib/piggly/util.rb +9 -0
  45. data/lib/piggly/version.rb +15 -0
  46. data/lib/piggly.rb +20 -0
  47. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  48. data/spec/examples/compiler/report_spec.rb +25 -0
  49. data/spec/examples/compiler/trace_spec.rb +123 -0
  50. data/spec/examples/config_spec.rb +63 -0
  51. data/spec/examples/dumper/index_spec.rb +199 -0
  52. data/spec/examples/dumper/procedure_spec.rb +116 -0
  53. data/spec/examples/grammar/expression_spec.rb +302 -0
  54. data/spec/examples/grammar/statements/assignment_spec.rb +70 -0
  55. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  56. data/spec/examples/grammar/statements/exception_spec.rb +78 -0
  57. data/spec/examples/grammar/statements/if_spec.rb +191 -0
  58. data/spec/examples/grammar/statements/loop_spec.rb +41 -0
  59. data/spec/examples/grammar/statements/sql_spec.rb +71 -0
  60. data/spec/examples/grammar/tokens/comment_spec.rb +58 -0
  61. data/spec/examples/grammar/tokens/datatype_spec.rb +58 -0
  62. data/spec/examples/grammar/tokens/identifier_spec.rb +74 -0
  63. data/spec/examples/grammar/tokens/keyword_spec.rb +44 -0
  64. data/spec/examples/grammar/tokens/label_spec.rb +40 -0
  65. data/spec/examples/grammar/tokens/literal_spec.rb +30 -0
  66. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  67. data/spec/examples/grammar/tokens/number_spec.rb +34 -0
  68. data/spec/examples/grammar/tokens/sqlkeywords_spec.rb +45 -0
  69. data/spec/examples/grammar/tokens/string_spec.rb +54 -0
  70. data/spec/examples/grammar/tokens/whitespace_spec.rb +40 -0
  71. data/spec/examples/installer_spec.rb +59 -0
  72. data/spec/examples/parser/nodes_spec.rb +73 -0
  73. data/spec/examples/parser/traversal_spec.rb +14 -0
  74. data/spec/examples/parser_spec.rb +118 -0
  75. data/spec/examples/profile_spec.rb +153 -0
  76. data/spec/examples/reporter/html/dsl_spec.rb +0 -0
  77. data/spec/examples/reporter/html/index_spec.rb +0 -0
  78. data/spec/examples/reporter/html_spec.rb +1 -0
  79. data/spec/examples/reporter_spec.rb +0 -0
  80. data/spec/examples/tags_spec.rb +285 -0
  81. data/spec/examples/task_spec.rb +0 -0
  82. data/spec/examples/util/cacheable_spec.rb +41 -0
  83. data/spec/examples/util/enumerable_spec.rb +64 -0
  84. data/spec/examples/util/file_spec.rb +40 -0
  85. data/spec/examples/util/process_queue_spec.rb +16 -0
  86. data/spec/examples/util/thunk_spec.rb +59 -0
  87. data/spec/examples/version_spec.rb +0 -0
  88. data/spec/issues/007_spec.rb +25 -0
  89. data/spec/issues/008_spec.rb +73 -0
  90. data/spec/issues/018_spec.rb +25 -0
  91. data/spec/issues/028_spec.rb +48 -0
  92. data/spec/issues/032_spec.rb +98 -0
  93. data/spec/issues/036_spec.rb +41 -0
  94. data/spec/spec_helper.rb +312 -0
  95. data/spec/spec_suite.rb +5 -0
  96. metadata +162 -0
@@ -0,0 +1,172 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ #
5
+ # Differs from SkeletonProcedure in that the procedure source code is stored
6
+ # as an instance variable.
7
+ #
8
+ class ReifiedProcedure < SkeletonProcedure
9
+
10
+ def initialize(source, oid, name, strict, secdef, setof, type, volatility, arg_modes, arg_names, arg_types, arg_defaults, prokind = "f", language = "plpgsql")
11
+ # Ensure source is UTF-8 encoded
12
+ @source = source.to_s.force_encoding('UTF-8').strip
13
+
14
+ if type.name == "record" and type.schema == "pg_catalog" and arg_modes.include?("t")
15
+ prefix = arg_modes.take_while{|m| m != "t" }.length
16
+ type = RecordType.new(arg_types[prefix..-1], arg_names[prefix..-1], arg_modes[prefix..-1], arg_defaults[prefix..-1])
17
+ arg_modes = arg_modes[0, prefix]
18
+ arg_types = arg_types[0, prefix]
19
+ arg_names = arg_names[0, prefix]
20
+ arg_defaults = arg_defaults[0, prefix]
21
+ setof = false
22
+ end
23
+
24
+ super(oid, name, strict, secdef, setof, type, volatility, arg_modes, arg_names, arg_types, arg_defaults, prokind, language)
25
+ end
26
+
27
+ # @return [String]
28
+ def source(config)
29
+ @source
30
+ end
31
+
32
+ # @return [void]
33
+ def store_source(config)
34
+ if @source.include?("$PIGGLY$")
35
+ raise "Procedure `#{@name}' is already instrumented. " +
36
+ "This means the original source wasn't restored after the " +
37
+ "last coverage run. You must restore the source manually."
38
+ end
39
+
40
+ File.open(source_path(config), "wb:UTF-8"){|io| io.write(@source) }
41
+ end
42
+
43
+ # @return [SkeletonProcedure]
44
+ def skeleton
45
+ SkeletonProcedure.new(@oid, @name, @strict, @secdef, @setof, @type,
46
+ @volatility, @arg_modes, @arg_names, @arg_types,
47
+ @arg_defaults, @prokind, @language)
48
+ end
49
+
50
+ def skeleton?
51
+ false
52
+ end
53
+ end
54
+
55
+ class << ReifiedProcedure
56
+ # Rewrite "i", "o", and "b", otherwise pass-through
57
+ MODES = Hash.new{|h,k| k }.update \
58
+ "i" => "in",
59
+ "o" => "out",
60
+ "b" => "inout",
61
+ "v" => "variadic"
62
+
63
+ # Rewrite "i", "v", and "s", otherwise pass-through
64
+ VOLATILITY = Hash.new{|h,k| k }.update \
65
+ "i" => "immutable",
66
+ "v" => "volatile",
67
+ "s" => "stable"
68
+
69
+ def mode(mode)
70
+ MODES[mode]
71
+ end
72
+
73
+ def volatility(mode)
74
+ VOLATILITY[mode]
75
+ end
76
+
77
+ def defaults(exprs, count, total)
78
+ exprs = if exprs.nil? then [] else exprs.split(", ") end
79
+
80
+ nreqd = total - count
81
+
82
+ if nreqd >= 0 and exprs.length == count
83
+ Array.new(nreqd) + exprs
84
+ else
85
+ raise "Couldn't parse default arguments"
86
+ end
87
+ end
88
+
89
+ # Returns a list of all PL/pgSQL stored procedures in the current database
90
+ #
91
+ # @return [Array<ReifiedProcedure>]
92
+ def all(connection)
93
+ connection.query(<<-SQL).map{|x| from_hash(x) }
94
+ select
95
+ pro.oid,
96
+ nschema.nspname as nschema,
97
+ pro.proname as name,
98
+ pro.proisstrict as strict,
99
+ pro.prosecdef as secdef,
100
+ pro.provolatile as volatility,
101
+ pro.proretset as setof,
102
+ rschema.nspname as tschema,
103
+ ret.typname as type,
104
+ pro.prosrc as source,
105
+ pro.pronargs as arg_count,
106
+ lang.lanname as language,
107
+ array_to_string(pro.proargmodes, ',') as arg_modes,
108
+ array_to_string(pro.proargnames, ',') as arg_names,
109
+ case when proallargtypes is not null then
110
+ -- use proalltypes array if its non-null
111
+ array_to_string(array(select format_type(proallargtypes[k], null)
112
+ from generate_series(array_lower(proallargtypes, 1),
113
+ array_upper(proallargtypes, 1)) as k), ',')
114
+ else
115
+ -- fallback to oidvector proargtypes
116
+ oidvectortypes(pro.proargtypes)
117
+ end as arg_types,
118
+ pro.pronargdefaults as arg_defaults_count,
119
+ coalesce(pg_get_expr(pro.proargdefaults, 0), '') as arg_defaults,
120
+ coalesce(pro.prokind, 'f') as prokind
121
+ from pg_proc as pro,
122
+ pg_type as ret,
123
+ pg_namespace as nschema,
124
+ pg_namespace as rschema,
125
+ pg_language as lang
126
+ where pro.pronamespace = nschema.oid
127
+ and pro.prolang = lang.oid
128
+ and ret.typnamespace = rschema.oid
129
+ and pro.proname not like 'piggly_%'
130
+ and pro.prorettype = ret.oid
131
+ and pro.prolang = (select oid from pg_language where lanname = 'plpgsql')
132
+ and pro.pronamespace not in (select oid
133
+ from pg_namespace
134
+ where nspname like 'pg_%'
135
+ or nspname like 'information_schema')
136
+ SQL
137
+ end
138
+
139
+ # Construct a ReifiedProcedure from a result row (Hash)
140
+ #
141
+ # @return [ReifiedProcedure]
142
+ def from_hash(hash)
143
+ new(hash["source"],
144
+ hash["oid"],
145
+ QualifiedName.new(hash["nschema"].to_s, hash["name"].to_s),
146
+ hash["strict"] == "t",
147
+ hash["secdef"] == "t",
148
+ hash["setof"] == "t",
149
+ QualifiedType.parse(hash["tschema"].to_s, hash["type"].to_s),
150
+ volatility(hash["volatility"]),
151
+ coalesce(hash["arg_modes"].to_s.split(",").map{|x| mode(x.strip) },
152
+ ["in"]*hash["arg_count"].to_i),
153
+ hash["arg_names"].to_s.split(",").map{|x| QualifiedName.new(nil, x.strip) },
154
+ hash["arg_types"].to_s.split(",").map{|x| QualifiedType.parse(x.strip) },
155
+ defaults(hash["arg_defaults"],
156
+ hash["arg_defaults_count"].to_i,
157
+ hash["arg_count"].to_i),
158
+ hash["prokind"].to_s,
159
+ hash["language"].to_s)
160
+ end
161
+
162
+ def coalesce(value, default)
163
+ if [nil, "", []].include?(value)
164
+ default
165
+ else
166
+ value
167
+ end
168
+ end
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,112 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ #
5
+ # Encapsulates all the information about a stored procedure, except the
6
+ # procedure's source code, which is assumed to be on disk, loaded as needed.
7
+ #
8
+ class SkeletonProcedure
9
+
10
+ attr_reader :oid, :name, :type, :arg_types, :arg_modes, :arg_names,
11
+ :strict, :setof, :volatility, :secdef, :identifier, :prokind, :language
12
+
13
+ def initialize(oid, name, strict, secdef, setof, type, volatility, arg_modes, arg_names, arg_types, arg_defaults, prokind = "f", language = "plpgsql")
14
+ @oid, @name, @strict, @secdef, @type, @volatility, @setof, @arg_modes, @arg_names, @arg_types, @arg_defaults, @prokind, @language =
15
+ oid, name, strict, secdef, type, volatility, setof, arg_modes, arg_names, arg_types, arg_defaults, prokind, language
16
+
17
+
18
+ @identifier = Digest::MD5.hexdigest(signature)
19
+ end
20
+
21
+ # Returns source text for argument list
22
+ # @return [String]
23
+ def arguments
24
+ @arg_types.zip(@arg_names, @arg_modes, @arg_defaults).map do |type, name, mode, default|
25
+ "#{mode + " " if mode}#{name.quote + " " if name}#{type.quote}#{" default " + default if default}"
26
+ end.join(", ")
27
+ end
28
+
29
+ # Returns source text for return type
30
+ # @return [String]
31
+ def setof
32
+ @setof ? "setof " : nil
33
+ end
34
+
35
+ # Returns source text for strictness
36
+ # @return [String]
37
+ def strictness
38
+ @strict ? "strict" : nil
39
+ end
40
+
41
+ # Returns source text for security
42
+ # @return [String]
43
+ def security
44
+ @secdef ? "security definer" : nil
45
+ end
46
+
47
+ # Returns source SQL function/procedure definition statement
48
+ # @return [String]
49
+ def definition(body)
50
+ if @prokind == 'p'
51
+ # PostgreSQL PROCEDURE (introduced in PG11)
52
+ [%[create or replace procedure #{name.quote} (#{arguments})],
53
+ %[ language plpgsql #{security} as $__PIGGLY__$],
54
+ body,
55
+ %[$__PIGGLY__$]].join("\n")
56
+ else
57
+ # PostgreSQL FUNCTION
58
+ [%[create or replace function #{name.quote} (#{arguments})],
59
+ %[ returns #{setof}#{type.quote} as $__PIGGLY__$],
60
+ body,
61
+ %[$__PIGGLY__$ language plpgsql #{strictness} #{security} #{@volatility}]].join("\n")
62
+ end
63
+ end
64
+
65
+ # @return [String]
66
+ def signature
67
+ "#{@name}(#{@arg_modes.zip(@arg_types).map{|m,t| "#{m} #{t}" }.join(", ")})"
68
+ end
69
+
70
+ # @return [String]
71
+ def source_path(config)
72
+ config.mkpath("#{config.cache_root}/Dumper", "#{@identifier}.plpgsql")
73
+ end
74
+
75
+ # @return [String]
76
+ def load_source(config)
77
+ File.read(source_path(config), encoding: 'UTF-8')
78
+ end
79
+
80
+ # @return [String]
81
+ alias source load_source
82
+
83
+ # @return [void]
84
+ def purge_source(config)
85
+ path = source_path(config)
86
+
87
+ FileUtils.rm_r(path) if File.exist?(path)
88
+
89
+ file = Compiler::TraceCompiler.new(config).cache_path(path)
90
+ FileUtils.rm_r(file) if File.exist?(file)
91
+
92
+ file = Reporter::Base.new(config).report_path(path, ".html")
93
+ FileUtils.rm_r(file) if File.exist?(file)
94
+ end
95
+
96
+ # @return [SkeletonProcedure]
97
+ def skeleton
98
+ self
99
+ end
100
+
101
+ def skeleton?
102
+ true
103
+ end
104
+
105
+ def ==(other)
106
+ other.is_a?(self.class) and
107
+ other.identifier == identifier
108
+ end
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,9 @@
1
+ module Piggly
2
+ module Dumper
3
+ autoload :Index, "piggly/dumper/index"
4
+ autoload :QualifiedName, "piggly/dumper/qualified_name"
5
+ autoload :QualifiedType, "piggly/dumper/qualified_type"
6
+ autoload :ReifiedProcedure, "piggly/dumper/reified_procedure"
7
+ autoload :SkeletonProcedure, "piggly/dumper/skeleton_procedure"
8
+ end
9
+ end
@@ -0,0 +1,137 @@
1
+ module Piggly
2
+
3
+ class Installer
4
+ def initialize(config, connection)
5
+ @config, @connection = config, connection
6
+ end
7
+
8
+ # @return [void]
9
+ def install(procedures, profile)
10
+ @connection.exec("begin")
11
+
12
+ install_support(profile)
13
+
14
+ procedures.each do |p|
15
+ begin
16
+ trace(p, profile)
17
+ rescue Parser::Failure
18
+ $stdout.puts $!
19
+ end
20
+ end
21
+
22
+ @connection.exec("commit")
23
+ rescue
24
+ @connection.exec("rollback")
25
+ raise
26
+ end
27
+
28
+ # @return [void]
29
+ def uninstall(procedures)
30
+ @connection.exec("begin")
31
+
32
+ procedures.each{|p| untrace(p) }
33
+ uninstall_support
34
+
35
+ @connection.exec("commit")
36
+ rescue
37
+ @connection.exec("rollback")
38
+ raise
39
+ end
40
+
41
+ # @return [void]
42
+ def trace(procedure, profile)
43
+ # recompile with instrumentation
44
+ compiler = Compiler::TraceCompiler.new(@config)
45
+ result = compiler.compile(procedure)
46
+ # result[:tree] - tagged and rewritten parse tree
47
+ # result[:tags] - collection of Tag values in the tree
48
+ # result[:code] - instrumented
49
+
50
+ @connection.exec(procedure.definition(result[:code]))
51
+
52
+ profile.add(procedure, result[:tags], result)
53
+ rescue
54
+ $!.message << "\nError installing traced procedure #{procedure.name} "
55
+ $!.message << "from #{procedure.source_path(@config)}"
56
+ raise
57
+ end
58
+
59
+ # @return [void]
60
+ def untrace(procedure)
61
+ @connection.exec(procedure.definition(procedure.source(@config)))
62
+ end
63
+
64
+ # Installs necessary instrumentation support
65
+ def install_support(profile)
66
+ @connection.set_notice_processor(&profile.notice_processor(@config))
67
+
68
+ # def connection.set_notice_processor
69
+ # # do nothing: prevent the notice processor from being subverted
70
+ # end
71
+
72
+ # install tracing functions
73
+ @connection.exec <<-SQL
74
+ -- Signals that a conditional expression was executed
75
+ CREATE OR REPLACE FUNCTION piggly_cond(message varchar, value boolean)
76
+ RETURNS boolean AS $$
77
+ BEGIN
78
+ IF value THEN
79
+ RAISE WARNING '#{@config.trace_prefix} % t', message;
80
+ ELSE
81
+ RAISE WARNING '#{@config.trace_prefix} % f', message;
82
+ END IF;
83
+ RETURN value;
84
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
85
+ SQL
86
+
87
+ @connection.exec <<-SQL
88
+ -- Generic signal
89
+ CREATE OR REPLACE FUNCTION piggly_signal(message varchar, signal varchar)
90
+ RETURNS void AS $$
91
+ BEGIN
92
+ RAISE WARNING '#{@config.trace_prefix} % %', message, signal;
93
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
94
+ SQL
95
+
96
+ @connection.exec <<-SQL
97
+ -- Signals that a (sub)expression was executed. handles '' and NULL value
98
+ CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value varchar)
99
+ RETURNS varchar AS $$
100
+ BEGIN
101
+ RAISE WARNING '#{@config.trace_prefix} %', message;
102
+ RETURN value;
103
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
104
+ SQL
105
+
106
+ @connection.exec <<-SQL
107
+ -- Signals that a (sub)expression was executed. handles all other types
108
+ CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value anyelement)
109
+ RETURNS anyelement AS $$
110
+ BEGIN
111
+ RAISE WARNING '#{@config.trace_prefix} %', message;
112
+ RETURN value;
113
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
114
+ SQL
115
+
116
+ @connection.exec <<-SQL
117
+ -- Signals that a branch was taken
118
+ CREATE OR REPLACE FUNCTION piggly_branch(message varchar)
119
+ RETURNS void AS $$
120
+ BEGIN
121
+ RAISE WARNING '#{@config.trace_prefix} %', message;
122
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
123
+ SQL
124
+ end
125
+
126
+ # Uninstalls instrumentation support
127
+ def uninstall_support
128
+ @connection.set_notice_processor{|x| $stderr.puts x }
129
+ @connection.exec "DROP FUNCTION IF EXISTS piggly_cond(varchar, boolean)"
130
+ @connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, varchar)"
131
+ @connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, anyelement)"
132
+ @connection.exec "DROP FUNCTION IF EXISTS piggly_branch(varchar)"
133
+ @connection.exec "DROP FUNCTION IF EXISTS piggly_signal(varchar, varchar)"
134
+ end
135
+ end
136
+
137
+ end