piggly 1.2.1 → 2.0.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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +163 -0
  3. data/Rakefile +29 -15
  4. data/bin/piggly +4 -244
  5. data/lib/piggly.rb +19 -17
  6. data/lib/piggly/command.rb +9 -0
  7. data/lib/piggly/command/base.rb +148 -0
  8. data/lib/piggly/command/report.rb +162 -0
  9. data/lib/piggly/command/test.rb +157 -0
  10. data/lib/piggly/command/trace.rb +90 -0
  11. data/lib/piggly/command/untrace.rb +78 -0
  12. data/lib/piggly/compiler.rb +7 -5
  13. data/lib/piggly/compiler/cache_dir.rb +119 -0
  14. data/lib/piggly/compiler/coverage_report.rb +63 -0
  15. data/lib/piggly/compiler/trace_compiler.rb +105 -0
  16. data/lib/piggly/config.rb +47 -22
  17. data/lib/piggly/dumper.rb +9 -0
  18. data/lib/piggly/dumper/index.rb +121 -0
  19. data/lib/piggly/dumper/qualified_name.rb +36 -0
  20. data/lib/piggly/dumper/qualified_type.rb +81 -0
  21. data/lib/piggly/dumper/reified_procedure.rb +142 -0
  22. data/lib/piggly/dumper/skeleton_procedure.rb +102 -0
  23. data/lib/piggly/installer.rb +84 -42
  24. data/lib/piggly/parser.rb +43 -49
  25. data/lib/piggly/parser/grammar.tt +289 -313
  26. data/lib/piggly/parser/nodes.rb +270 -211
  27. data/lib/piggly/parser/traversal.rb +35 -33
  28. data/lib/piggly/parser/treetop_ruby19_patch.rb +1 -1
  29. data/lib/piggly/profile.rb +81 -60
  30. data/lib/piggly/reporter.rb +5 -18
  31. data/lib/piggly/reporter/base.rb +103 -0
  32. data/lib/piggly/reporter/html_dsl.rb +63 -0
  33. data/lib/piggly/reporter/index.rb +108 -0
  34. data/lib/piggly/reporter/procedure.rb +104 -0
  35. data/lib/piggly/reporter/resources/highlight.js +21 -0
  36. data/lib/piggly/reporter/{piggly.css → resources/piggly.css} +52 -12
  37. data/lib/piggly/reporter/{sortable.js → resources/sortable.js} +0 -0
  38. data/lib/piggly/tags.rb +280 -0
  39. data/lib/piggly/task.rb +191 -40
  40. data/lib/piggly/util.rb +8 -27
  41. data/lib/piggly/util/blankslate.rb +114 -0
  42. data/lib/piggly/util/cacheable.rb +19 -0
  43. data/lib/piggly/util/enumerable.rb +44 -0
  44. data/lib/piggly/util/file.rb +17 -0
  45. data/lib/piggly/util/process_queue.rb +96 -0
  46. data/lib/piggly/util/thunk.rb +39 -0
  47. data/lib/piggly/version.rb +8 -8
  48. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  49. data/spec/examples/compiler/report_spec.rb +25 -0
  50. data/spec/{compiler → examples/compiler}/trace_spec.rb +7 -57
  51. data/spec/examples/config_spec.rb +61 -0
  52. data/spec/examples/dumper/index_spec.rb +197 -0
  53. data/spec/examples/dumper/procedure_spec.rb +116 -0
  54. data/spec/{grammar → examples/grammar}/expression_spec.rb +60 -60
  55. data/spec/{grammar → examples/grammar}/statements/assignment_spec.rb +15 -15
  56. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  57. data/spec/{grammar → examples/grammar}/statements/exception_spec.rb +10 -10
  58. data/spec/{grammar → examples/grammar}/statements/if_spec.rb +47 -34
  59. data/spec/{grammar → examples/grammar}/statements/loop_spec.rb +5 -5
  60. data/spec/{grammar → examples/grammar}/statements/sql_spec.rb +11 -11
  61. data/spec/{grammar → examples/grammar}/tokens/comment_spec.rb +11 -11
  62. data/spec/{grammar → examples/grammar}/tokens/datatype_spec.rb +14 -8
  63. data/spec/{grammar → examples/grammar}/tokens/identifier_spec.rb +26 -10
  64. data/spec/{grammar → examples/grammar}/tokens/keyword_spec.rb +5 -5
  65. data/spec/{grammar → examples/grammar}/tokens/label_spec.rb +7 -7
  66. data/spec/{grammar → examples/grammar}/tokens/literal_spec.rb +1 -1
  67. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  68. data/spec/{grammar → examples/grammar}/tokens/number_spec.rb +1 -1
  69. data/spec/{grammar → examples/grammar}/tokens/sqlkeywords_spec.rb +1 -1
  70. data/spec/{grammar → examples/grammar}/tokens/string_spec.rb +9 -9
  71. data/spec/{grammar → examples/grammar}/tokens/whitespace_spec.rb +1 -1
  72. data/spec/examples/installer_spec.rb +59 -0
  73. data/spec/examples/parser/nodes_spec.rb +73 -0
  74. data/spec/examples/parser/traversal_spec.rb +14 -0
  75. data/spec/examples/parser_spec.rb +115 -0
  76. data/spec/examples/profile_spec.rb +153 -0
  77. data/spec/{reporter/html_spec.rb → examples/reporter/html/dsl_spec.rb} +0 -0
  78. data/spec/examples/reporter/html/index_spec.rb +0 -0
  79. data/spec/examples/reporter/html_spec.rb +1 -0
  80. data/spec/examples/reporter_spec.rb +0 -0
  81. data/spec/{compiler → examples}/tags_spec.rb +10 -10
  82. data/spec/examples/task_spec.rb +0 -0
  83. data/spec/examples/util/cacheable_spec.rb +41 -0
  84. data/spec/examples/util/enumerable_spec.rb +64 -0
  85. data/spec/examples/util/file_spec.rb +40 -0
  86. data/spec/examples/util/process_queue_spec.rb +16 -0
  87. data/spec/examples/util/thunk_spec.rb +58 -0
  88. data/spec/examples/version_spec.rb +0 -0
  89. data/spec/issues/007_spec.rb +25 -0
  90. data/spec/issues/008_spec.rb +73 -0
  91. data/spec/issues/018_spec.rb +25 -0
  92. data/spec/spec_helper.rb +253 -9
  93. metadata +136 -93
  94. data/README.markdown +0 -116
  95. data/lib/piggly/compiler/cache.rb +0 -151
  96. data/lib/piggly/compiler/pretty.rb +0 -67
  97. data/lib/piggly/compiler/queue.rb +0 -46
  98. data/lib/piggly/compiler/tags.rb +0 -244
  99. data/lib/piggly/compiler/trace.rb +0 -91
  100. data/lib/piggly/filecache.rb +0 -40
  101. data/lib/piggly/parser/parser.rb +0 -11794
  102. data/lib/piggly/reporter/html.rb +0 -207
  103. data/spec/compiler/cache_spec.rb +0 -9
  104. data/spec/compiler/pretty_spec.rb +0 -9
  105. data/spec/compiler/queue_spec.rb +0 -3
  106. data/spec/compiler/rewrite_spec.rb +0 -3
  107. data/spec/config_spec.rb +0 -58
  108. data/spec/filecache_spec.rb +0 -70
  109. data/spec/fixtures/snippets.sql +0 -158
  110. data/spec/grammar/tokens/lval_spec.rb +0 -50
  111. data/spec/parser_spec.rb +0 -8
  112. data/spec/profile_spec.rb +0 -5
@@ -1,34 +1,19 @@
1
1
  module Piggly
2
2
  class Config
3
+ end
3
4
 
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
5
+ class << Config
6
+ def path(root, file=nil)
7
+ if file.nil?
8
+ root
9
+ else
23
10
  file[%r{^\.\.|^\/|^(?:[A-Z]:)?/}i] ?
24
11
  file : # ../path, /path, or D:\path that isn't relative to root
25
12
  File.join(root, file)
26
- else
27
- root
28
13
  end
29
14
  end
30
15
 
31
- def self.mkpath(root, file=nil)
16
+ def mkpath(root, file=nil)
32
17
  if file.nil?
33
18
  FileUtils.makedirs(root)
34
19
  root
@@ -39,5 +24,45 @@ module Piggly
39
24
  end
40
25
  end
41
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
+ config_accessor \
49
+ :cache_root => File.expand_path("#{Dir.pwd}/piggly/cache"),
50
+ :report_root => File.expand_path("#{Dir.pwd}/piggly/reports"),
51
+ :database_yml => nil,
52
+ :connection_name => "piggly",
53
+ :trace_prefix => "PIGGLY",
54
+ :accumulate => false,
55
+ :dry_run => false,
56
+ :filters => []
57
+
58
+ alias accumulate? accumulate
59
+
60
+ def path(*args)
61
+ self.class.path(*args)
62
+ end
63
+
64
+ def mkpath(*args)
65
+ self.class.mkpath(*args)
66
+ end
42
67
  end
43
68
  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,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.exists?(path)
106
+ []
107
+ else
108
+ YAML.load(File.read(path))
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"){|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,81 @@
1
+ module Piggly
2
+ module Dumper
3
+
4
+ class QualifiedType
5
+ attr_reader :schema, :name
6
+
7
+ def initialize(schema, name)
8
+ @schema, @name = schema, normalize(name)
9
+ end
10
+
11
+ def shorten
12
+ self.class.new(nil, @name)
13
+ end
14
+
15
+ def quote
16
+ if @schema
17
+ '"' + @schema + '"."' + @name + '"'
18
+ else
19
+ '"' + @name + '"'
20
+ end
21
+ end
22
+
23
+ def to_s
24
+ if @schema and !%w[public pg_catalog].include?(@schema)
25
+ @schema + "." + readable(@name)
26
+ else
27
+ readable(@name)
28
+ end
29
+ end
30
+
31
+ def ==(other)
32
+ self.to_s == other.to_s
33
+ end
34
+
35
+ protected
36
+
37
+ def normalize(name)
38
+ # select format_type(ret.oid, null), ret.typname
39
+ # from pg_type as ret
40
+ # where ret.typname <> format_type(ret.oid, null)
41
+ # and ret.typname not like '\\_%'
42
+ # group by ret.typname, format_type(ret.oid, null)
43
+ # order by format_type(ret.oid, null);
44
+ case name
45
+ when /(.*)\[\]/ then "_#{normalize($1)}"
46
+ when '"any"' then "any"
47
+ when "bigint" then "int8"
48
+ when "bit varying" then "varbit"
49
+ when "boolean" then "bool"
50
+ when '"char"' then "char"
51
+ when "character" then "bpchar"
52
+ when "character varying" then "varchar"
53
+ when "double precision" then "float8"
54
+ when "information_schema\.(.*)" then $1
55
+ when "integer" then "int4"
56
+ when "real" then "float4"
57
+ when "smallint" then "int2"
58
+ when "timestamp without time zone" then "timestamp"
59
+ when "timestamp with time zone" then "timestamptz"
60
+ when "time without time zone" then "time"
61
+ when "time with time zone" then "timetz"
62
+ else name
63
+ end
64
+ end
65
+
66
+ def readable(name)
67
+ case name
68
+ when /^_(.*)/ then "#{readable($1)}[]"
69
+ when "bpchar" then "char"
70
+ when /^float4(.*)/ then "real#{$1}"
71
+ when /^int2(.*)/ then "smallint#{$1}"
72
+ when /^int4(.*)/ then "int#{$1}"
73
+ when /^int8(.*)/ then "bigint#{$1}"
74
+ when /^serial4(.*)/ then "serial#{$1}"
75
+ else name
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,142 @@
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, *args)
11
+ super(*args)
12
+ @source = source.strip
13
+ end
14
+
15
+ # @return [String]
16
+ def source(config)
17
+ @source
18
+ end
19
+
20
+ # @return [void]
21
+ def store_source(config)
22
+ if @source.include?("$PIGGLY$")
23
+ raise "Procedure `#{@name}' is already instrumented. " +
24
+ "This means the original source wasn't restored after the " +
25
+ "last coverage run. You must restore the source manually."
26
+ end
27
+
28
+ File.open(source_path(config), "wb"){|io| io.write(@source) }
29
+ end
30
+
31
+ # @return [SkeletonProcedure]
32
+ def skeleton
33
+ SkeletonProcedure.new(@oid, @name, @strict, @secdef, @setof, @type,
34
+ @volatility, @arg_modes, @arg_names, @arg_types,
35
+ @arg_defaults)
36
+ end
37
+
38
+ def skeleton?
39
+ false
40
+ end
41
+ end
42
+
43
+ class << ReifiedProcedure
44
+ # Rewrite "i", "o", and "b", otherwise pass-through
45
+ MODES = Hash.new{|h,k| k }.update \
46
+ "i" => "in",
47
+ "o" => "out",
48
+ "b" => "inout",
49
+ "v" => "variadic"
50
+
51
+ # Rewrite "i", "v", and "s", otherwise pass-through
52
+ VOLATILITY = Hash.new{|h,k| k }.update \
53
+ "i" => "immutable",
54
+ "v" => "volatile",
55
+ "s" => "stable"
56
+
57
+ def mode(mode)
58
+ MODES[mode]
59
+ end
60
+
61
+ def volatility(mode)
62
+ VOLATILITY[mode]
63
+ end
64
+
65
+ def defaults(exprs, count, total)
66
+ exprs = exprs.split(", ")
67
+ nreqd = total - count
68
+
69
+ if nreqd > 0 and exprs.length == count
70
+ Array.new(nreqd) + exprs
71
+ else
72
+ raise "Couldn't parse default arguments"
73
+ end
74
+ end
75
+
76
+ # Returns a list of all PL/pgSQL stored procedures in the current database
77
+ #
78
+ # @return [Array<ReifiedProcedure>]
79
+ def all(connection)
80
+ connection.query(<<-SQL).map{|x| from_hash(x) }
81
+ select
82
+ pro.oid,
83
+ nschema.nspname as nschema,
84
+ pro.proname as name,
85
+ pro.proisstrict as strict,
86
+ pro.prosecdef as secdef,
87
+ pro.provolatile as volatility,
88
+ pro.proretset as setof,
89
+ rschema.nspname as tschema,
90
+ ret.typname as type,
91
+ pro.prosrc as source,
92
+ pro.pronargs as arg_count,
93
+ array_to_string(pro.proargmodes, ',') as arg_modes,
94
+ array_to_string(pro.proargnames, ',') as arg_names,
95
+ case when proallargtypes is not null then
96
+ -- use proalltypes array if its non-null
97
+ array_to_string(array(select format_type(proallargtypes[k], null)
98
+ from generate_series(array_lower(proallargtypes, 1),
99
+ array_upper(proallargtypes, 1)) as k), ',')
100
+ else
101
+ -- fallback to oidvector proargtypes
102
+ oidvectortypes(pro.proargtypes)
103
+ end as arg_types,
104
+ pro.pronargdefaults as arg_defaults_count,
105
+ coalesce(pg_get_expr(pro.proargdefaults, 0), '') as arg_defaults
106
+ from pg_proc as pro,
107
+ pg_type as ret,
108
+ pg_namespace as nschema,
109
+ pg_namespace as rschema
110
+ where pro.pronamespace = nschema.oid
111
+ and ret.typnamespace = rschema.oid
112
+ and pro.proname not like 'piggly_%'
113
+ and pro.prorettype = ret.oid
114
+ and pro.prolang = (select oid from pg_language where lanname = 'plpgsql')
115
+ and pro.pronamespace not in (select oid
116
+ from pg_namespace
117
+ where nspname like 'pg_%'
118
+ or nspname like 'information_schema')
119
+ SQL
120
+ end
121
+
122
+ # Construct a ReifiedProcedure from a result row (Hash)
123
+ #
124
+ # @return [ReifiedProcedure]
125
+ def from_hash(hash)
126
+ new(hash["source"],
127
+ hash["oid"],
128
+ QualifiedName.new(hash["nschema"], hash["name"]),
129
+ hash["strict"] == "t",
130
+ hash["secdef"] == "t",
131
+ hash["setof"] == "t",
132
+ QualifiedType.new(hash["tschema"], hash["type"]),
133
+ volatility(hash["volatility"]),
134
+ hash["arg_modes"].to_s.split(",").map{|x| mode(x.strip) },
135
+ hash["arg_names"].to_s.split(",").map{|x| QualifiedName.new(nil, x.strip) },
136
+ hash["arg_types"].to_s.split(",").map{|x| QualifiedType.new(nil, x.strip) },
137
+ defaults(hash["arg_defaults"], hash["arg_defaults_count"].to_i, hash["arg_count"].to_i))
138
+ end
139
+ end
140
+
141
+ end
142
+ end