piggly 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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