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,199 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
module Piggly
|
|
4
|
+
|
|
5
|
+
describe Dumper::Index do
|
|
6
|
+
before do
|
|
7
|
+
# make sure not to create directories all over the file system during the test
|
|
8
|
+
allow(Config).to receive(:mkpath) { |root, file| File.join(root, file) }
|
|
9
|
+
|
|
10
|
+
@config = Config.new
|
|
11
|
+
@index = Dumper::Index.new(@config)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "when cache file doesn't exist" do
|
|
15
|
+
it "is empty" do
|
|
16
|
+
expect(File).to receive(:exist?).with(@index.path).and_return(false)
|
|
17
|
+
expect(@index.procedures).to be_empty
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context "when cache file exists" do
|
|
22
|
+
before do
|
|
23
|
+
allow(File).to receive(:exist?).with(@index.path).and_return(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context "when the cache index file is empty" do
|
|
27
|
+
it "is empty" do
|
|
28
|
+
allow(File).to receive(:read).and_return([].to_yaml)
|
|
29
|
+
expect(@index.procedures).to be_empty
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "when the cache index file has two entries" do
|
|
34
|
+
before do
|
|
35
|
+
@first = Dumper::ReifiedProcedure.from_hash \
|
|
36
|
+
"oid" => "1000",
|
|
37
|
+
"name" => "iterate",
|
|
38
|
+
"source" => "FIRST PROCEDURE SOURCE CODE"
|
|
39
|
+
|
|
40
|
+
@second = Dumper::ReifiedProcedure.from_hash \
|
|
41
|
+
"oid" => "2000",
|
|
42
|
+
"name" => "login",
|
|
43
|
+
"source" => "SECOND PROCEDURE SOURCE CODE"
|
|
44
|
+
|
|
45
|
+
allow(File).to receive(:read) do |path, *args|
|
|
46
|
+
case path
|
|
47
|
+
when @first.source_path(@config)
|
|
48
|
+
@first.source(@config)
|
|
49
|
+
when @second.source_path(@config)
|
|
50
|
+
@second.source(@config)
|
|
51
|
+
when @index.path
|
|
52
|
+
YAML.dump([@first, @second])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "has two procedures" do
|
|
58
|
+
expect(@index.procedures.size).to eq(2)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "is indexed by identifier" do
|
|
62
|
+
expect(@index[@first.identifier].identifier).to eq(@first.identifier)
|
|
63
|
+
expect(@index[@second.identifier].identifier).to eq(@second.identifier)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "reads each procedure's source_path" do
|
|
67
|
+
expect(@index[@first.identifier].source(@config)).to eq(@first.source(@config))
|
|
68
|
+
expect(@index[@second.identifier].source(@config)).to eq(@second.source(@config))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context "when the procedures used to be identified using another method" do
|
|
72
|
+
it "renames each procedure using the current identifier"
|
|
73
|
+
it "updates the index with the current identified_using"
|
|
74
|
+
it "writes the updated index to disk"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "update" do
|
|
80
|
+
it "caches the source of new procedures"
|
|
81
|
+
it "updates the cached source of updated procedures"
|
|
82
|
+
it "purges the cached source of outdated procedures"
|
|
83
|
+
it "writes the cache index to disk"
|
|
84
|
+
it "does not write procedure source code within the cache index"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "label" do
|
|
88
|
+
def q(*ns)
|
|
89
|
+
Dumper::QualifiedName.new(*ns)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
before do
|
|
93
|
+
@procedure = double(:oid => 1,
|
|
94
|
+
:name => q("public", "foo"),
|
|
95
|
+
:type => q("private", "int"),
|
|
96
|
+
:arg_modes => ["in", "in"],
|
|
97
|
+
:arg_names => [],
|
|
98
|
+
:arg_types => [q("private", "int"), q("private", "varchar")])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "when name is unique" do
|
|
102
|
+
context "and there is only one schema" do
|
|
103
|
+
before do
|
|
104
|
+
allow(@index).to receive(:procedures).and_return(
|
|
105
|
+
[ @procedure,
|
|
106
|
+
double(:oid => 2,
|
|
107
|
+
:name => q("public", "bar"),
|
|
108
|
+
:type => q("private", "int"),
|
|
109
|
+
:arg_modes => ["in"],
|
|
110
|
+
:arg_names => [],
|
|
111
|
+
:arg_types => []) ])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "specifies schema.name" do
|
|
115
|
+
expect(@index.label(@procedure)).to eq("foo")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context "and there is more than one schema" do
|
|
120
|
+
before do
|
|
121
|
+
allow(@index).to receive(:procedures).and_return(
|
|
122
|
+
[ @procedure,
|
|
123
|
+
double(:oid => 2,
|
|
124
|
+
:name => q("schema", "foo"),
|
|
125
|
+
:type => q("private", "int"),
|
|
126
|
+
:arg_modes => ["in"],
|
|
127
|
+
:arg_names => [],
|
|
128
|
+
:arg_types => []) ])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "specifies schema.name" do
|
|
132
|
+
expect(@index.label(@procedure)).to eq("public.foo")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context "when name is not unique" do
|
|
138
|
+
context "and schema.name is unique" do
|
|
139
|
+
before do
|
|
140
|
+
allow(@index).to receive(:procedures).and_return(
|
|
141
|
+
[ @procedure,
|
|
142
|
+
double(:oid => 2,
|
|
143
|
+
:name => q("schema", "foo"),
|
|
144
|
+
:type => q("private", "int"),
|
|
145
|
+
:arg_modes => ["in"],
|
|
146
|
+
:arg_names => [],
|
|
147
|
+
:arg_types => []) ])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "specifies schema.name" do
|
|
151
|
+
expect(@index.label(@procedure)).to eq("public.foo")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context "and schema.name is not unique" do
|
|
156
|
+
context "but argument types are unique" do
|
|
157
|
+
before do
|
|
158
|
+
allow(@index).to receive(:procedures).and_return(
|
|
159
|
+
[ @procedure,
|
|
160
|
+
double(:oid => 2,
|
|
161
|
+
:name => q("public", "foo"),
|
|
162
|
+
:type => q("private", "int"),
|
|
163
|
+
:arg_modes => ["in"],
|
|
164
|
+
:arg_names => [],
|
|
165
|
+
:arg_types => []) ])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "specifies schema.name(types)" do
|
|
169
|
+
expect(@index.label(@procedure)).to eq(
|
|
170
|
+
"foo(private.int, private.varchar)")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
context "and argument types are not unique" do
|
|
175
|
+
context "but argument modes are unique" do
|
|
176
|
+
before do
|
|
177
|
+
allow(@index).to receive(:procedures).and_return(
|
|
178
|
+
[ @procedure,
|
|
179
|
+
double(:oid => 2,
|
|
180
|
+
:name => q("public", "foo"),
|
|
181
|
+
:type => q("private", "int"),
|
|
182
|
+
:arg_modes => ["out", "out"],
|
|
183
|
+
:arg_names => [],
|
|
184
|
+
:arg_types => [q("private", "int"), q("private", "varchar")]) ])
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "specifies schema.name(types and modes)" do
|
|
188
|
+
expect(@index.label(@procedure)).to eq(
|
|
189
|
+
"foo(in private.int, in private.varchar)")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Piggly
|
|
4
|
+
|
|
5
|
+
describe Dumper::ReifiedProcedure do
|
|
6
|
+
describe "all" do
|
|
7
|
+
before do
|
|
8
|
+
# stub connection
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "from_hash" do
|
|
13
|
+
it "abbreviates known return types"
|
|
14
|
+
it "leaves alone unknown argument types"
|
|
15
|
+
|
|
16
|
+
it "abbreviates known return types"
|
|
17
|
+
it "leaves alone unknown argument types"
|
|
18
|
+
|
|
19
|
+
it "maps known volatilities"
|
|
20
|
+
it "leaves alone unknown volatilities"
|
|
21
|
+
|
|
22
|
+
it "maps known argument modes"
|
|
23
|
+
it "leaves alone unknown argument modes"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "store_source" do
|
|
27
|
+
context "when source is already instrumented" do
|
|
28
|
+
it "raises an error"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context "when the procedure was identified using the current configuration setting" do
|
|
32
|
+
it "does not attempt to remove any files"
|
|
33
|
+
it "writes to the current location"
|
|
34
|
+
it "has the current identified_using property"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when the procedure was identified using some other configuration setting" do
|
|
38
|
+
it "removes any old report files"
|
|
39
|
+
it "removes any old trace cache files"
|
|
40
|
+
it "removes the old source cache files"
|
|
41
|
+
it "writes to the current location"
|
|
42
|
+
it "updates the identified_using property"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe Dumper::SkeletonProcedure do
|
|
48
|
+
describe "definition" do
|
|
49
|
+
it "specifies namespace and function name"
|
|
50
|
+
it "specifies source code between dollar-quoted string tags"
|
|
51
|
+
|
|
52
|
+
context "with argument modes" do
|
|
53
|
+
it "specifies argument modes"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "without argument modes" do
|
|
57
|
+
it "doesn't specify argument modes"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context "with strict modifier" do
|
|
61
|
+
it "specifies STRICT token"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "without strict modifier" do
|
|
65
|
+
it "doesn't specify STRICT token"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context "with security definer modifier" do
|
|
69
|
+
it "specifies SECURITY DEFINER token"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context "without security definer modifier" do
|
|
73
|
+
it "doesn't specify SECURITY DEFINER token"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context "with set-returning type" do
|
|
77
|
+
it "specifies SETOF token"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "without non set-returning type" do
|
|
81
|
+
it "doesn't specify SETOF token"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context "with stable volatility" do
|
|
85
|
+
it "specifies STABLE token"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe "source_path" do
|
|
90
|
+
it "has a .plpgsql extension"
|
|
91
|
+
it "is within the Dumper directory"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "purge_source" do
|
|
95
|
+
context "when the procedure was identified using the current configuration setting" do
|
|
96
|
+
it "removes any old report files"
|
|
97
|
+
it "removes any old trace cache files"
|
|
98
|
+
it "removes the old source cache files"
|
|
99
|
+
it "removes the current report files"
|
|
100
|
+
it "removes the current trace cache files"
|
|
101
|
+
it "removes the current source cache files"
|
|
102
|
+
it "doesn't attempt to remove any other files"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context "when the procedure was identified using some other configuration setting" do
|
|
106
|
+
it "removes the current report files"
|
|
107
|
+
it "removes the current trace cache files"
|
|
108
|
+
it "removes the current source cache files"
|
|
109
|
+
it "doesn't attempt to remove any other files"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "equality operator"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Piggly
|
|
4
|
+
describe Parser, "expressions" do
|
|
5
|
+
include GrammarHelper
|
|
6
|
+
|
|
7
|
+
describe "expressionUntilSemiColon" do
|
|
8
|
+
it "does not consume semicolon" do
|
|
9
|
+
# parser stops in front of THEN and dies
|
|
10
|
+
expect{ parse(:expressionUntilSemiColon, 'abc;') }.to raise_error(Piggly::Parser::Failure)
|
|
11
|
+
|
|
12
|
+
node, rest = parse_some(:expressionUntilSemiColon, 'abc; xyz')
|
|
13
|
+
rest.should == '; xyz'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "can be a blank expression" do
|
|
17
|
+
node, rest = parse_some(:expressionUntilSemiColon, ';')
|
|
18
|
+
node.should be_expression
|
|
19
|
+
node.source_text.should == ''
|
|
20
|
+
rest.should == ';'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "can be a comment" do
|
|
24
|
+
node, rest = parse_some(:expressionUntilSemiColon, "/* comment */;")
|
|
25
|
+
node.should be_expression
|
|
26
|
+
node.count{|e| e.comment? }.should == 1
|
|
27
|
+
|
|
28
|
+
node, rest = parse_some(:expressionUntilSemiColon, "-- comment\n;")
|
|
29
|
+
node.should be_expression
|
|
30
|
+
node.count{|e| e.comment? }.should == 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "can be a string" do
|
|
34
|
+
node, rest = parse_some(:expressionUntilSemiColon, "'string';")
|
|
35
|
+
node.should be_expression
|
|
36
|
+
node.count{|e| e.string? }.should == 1
|
|
37
|
+
|
|
38
|
+
node, rest = parse_some(:expressionUntilSemiColon, "$$ string $$;")
|
|
39
|
+
node.should be_expression
|
|
40
|
+
node.count{|e| e.string? }.should == 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "can be an arithmetic expression" do
|
|
44
|
+
node, rest = parse_some(:expressionUntilSemiColon, "10 * (3 + x);")
|
|
45
|
+
node.should be_expression
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "can be an SQL statement" do
|
|
49
|
+
node, rest = parse_some(:expressionUntilSemiColon, "SELECT id FROM dataset;")
|
|
50
|
+
node.should be_expression
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "can be an expression with comments embedded" do
|
|
54
|
+
node, rest = parse_some(:expressionUntilSemiColon, <<-SQL)
|
|
55
|
+
SELECT id -- primary key ;
|
|
56
|
+
FROM "dataset" /* ; */ -- previous comments shouldn't terminate expression
|
|
57
|
+
WHERE value IS /*NOT*/ NULL;
|
|
58
|
+
SQL
|
|
59
|
+
node.should be_expression
|
|
60
|
+
node.count{|e| e.comment? }.should == 4
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "can be an expression with strings and comments embedded" do
|
|
64
|
+
node, rest = parse_some(:expressionUntilSemiColon, <<-SQL)
|
|
65
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
66
|
+
FROM dataset /* 2. this one's no problem either */
|
|
67
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
68
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
69
|
+
/* 4. farewell comment in tail */;
|
|
70
|
+
SQL
|
|
71
|
+
node.should be_expression
|
|
72
|
+
node.count{|e| e.comment? }.should == 4
|
|
73
|
+
node.count{|e| e.string? }.should == 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "can be an expression with strings embedded" do
|
|
77
|
+
node, rest = parse_some(:expressionUntilSemiColon, <<-SQL)
|
|
78
|
+
SELECT id, created_at
|
|
79
|
+
FROM "dataset"
|
|
80
|
+
WHERE value IS NOT NULL
|
|
81
|
+
AND value <> '; this should not terminate expression'
|
|
82
|
+
AND created_at = '2001-01-01';
|
|
83
|
+
SQL
|
|
84
|
+
node.should be_expression
|
|
85
|
+
node.count{|e| e.string? }.should == 2
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "combines trailing whitespace into 'tail' node" do
|
|
89
|
+
node, rest = parse_some(:expressionUntilSemiColon, "a := x + y \t;")
|
|
90
|
+
node.should be_expression
|
|
91
|
+
node.tail.source_text.should == " \t"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "combines trailing comments into 'tail' node" do
|
|
95
|
+
node, rest = parse_some(:expressionUntilSemiColon, "a := x + y /* note -- comment */;")
|
|
96
|
+
node.should be_expression
|
|
97
|
+
node.tail.source_text.should == ' /* note -- comment */'
|
|
98
|
+
|
|
99
|
+
node, rest = parse_some(:expressionUntilSemiColon, <<-SQL)
|
|
100
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
101
|
+
FROM dataset /* 2. this one's no problem either */
|
|
102
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
103
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
104
|
+
/* 4. farewell comment in tail */;
|
|
105
|
+
SQL
|
|
106
|
+
node.tail.count{|e| e.comment? }.should == 2
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "expressionUntilThen" do
|
|
111
|
+
it "does not consume THEN token" do
|
|
112
|
+
# parser stops in front of THEN and dies
|
|
113
|
+
expect{ parse(:expressionUntilThen, 'abc THEN') }.to raise_error(Piggly::Parser::Failure)
|
|
114
|
+
|
|
115
|
+
node, rest = parse_some(:expressionUntilThen, 'abc THEN xyz')
|
|
116
|
+
rest.should == 'THEN xyz'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "cannot be a blank expression" do
|
|
120
|
+
expect{ parse_some(:expressionUntilThen, ' THEN') }.to raise_error(Piggly::Parser::Failure)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "cannot be a comment" do
|
|
124
|
+
expect{ parse_some(:expressionUntilThen, "/* comment */ THEN") }.to raise_error(Piggly::Parser::Failure)
|
|
125
|
+
expect{ parse_some(:expressionUntilThen, "-- comment\n THEN") }.to raise_error(Piggly::Parser::Failure)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "can be a string" do
|
|
129
|
+
node, rest = parse_some(:expressionUntilThen, "'string' THEN")
|
|
130
|
+
node.should be_expression
|
|
131
|
+
node.count{|e| e.string? }.should == 1
|
|
132
|
+
|
|
133
|
+
node, rest = parse_some(:expressionUntilThen, "$$ string $$ THEN")
|
|
134
|
+
node.should be_expression
|
|
135
|
+
node.count{|e| e.string? }.should == 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "can be an arithmetic expression" do
|
|
139
|
+
node, rest = parse_some(:expressionUntilThen, "10 * (3 + x) THEN")
|
|
140
|
+
node.should be_expression
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "can be an SQL statement" do
|
|
144
|
+
node, rest = parse_some(:expressionUntilThen, "SELECT id FROM dataset THEN")
|
|
145
|
+
node.should be_expression
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "can be an expression with comments embedded" do
|
|
149
|
+
node, rest = parse_some(:expressionUntilThen, <<-SQL)
|
|
150
|
+
SELECT id -- primary key THEN
|
|
151
|
+
FROM "dataset" /* THEN */ -- previous comments shouldn't terminate expression
|
|
152
|
+
WHERE value IS /*NOT*/ NULL THEN
|
|
153
|
+
SQL
|
|
154
|
+
node.should be_expression
|
|
155
|
+
node.count{|e| e.comment? }.should == 4
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "can be an expression with strings and comments embedded" do
|
|
159
|
+
node, rest = parse_some(:expressionUntilThen, <<-SQL)
|
|
160
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
161
|
+
FROM dataset /* 2. this one's no problem either */
|
|
162
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
163
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
164
|
+
/* 4. farewell comment in tail */ THEN
|
|
165
|
+
SQL
|
|
166
|
+
node.should be_expression
|
|
167
|
+
node.count{|e| e.comment? }.should == 4
|
|
168
|
+
node.count{|e| e.string? }.should == 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "can be an expression with strings embedded" do
|
|
172
|
+
node, rest = parse_some(:expressionUntilThen, <<-SQL)
|
|
173
|
+
SELECT id, created_at
|
|
174
|
+
FROM "dataset"
|
|
175
|
+
WHERE value IS NOT NULL
|
|
176
|
+
AND value <> ' THEN this should not terminate expression'
|
|
177
|
+
AND created_at = '2001-01-01' THEN
|
|
178
|
+
SQL
|
|
179
|
+
node.should be_expression
|
|
180
|
+
node.count{|e| e.string? }.should == 2
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "combines trailing whitespace into 'tail' node" do
|
|
184
|
+
node, rest = parse_some(:expressionUntilThen, "a := x + y \tTHEN")
|
|
185
|
+
node.should be_expression
|
|
186
|
+
node.tail.source_text.should == " \t"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "combines trailing comments into 'tail' node" do
|
|
190
|
+
node, rest = parse_some(:expressionUntilThen, "a := x + y /* note -- comment */THEN")
|
|
191
|
+
node.should be_expression
|
|
192
|
+
node.tail.source_text.should == ' /* note -- comment */'
|
|
193
|
+
|
|
194
|
+
node, rest = parse_some(:expressionUntilThen, <<-SQL)
|
|
195
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
196
|
+
FROM dataset /* 2. this one's no problem either */
|
|
197
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
198
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
199
|
+
/* 4. farewell comment in tail */THEN
|
|
200
|
+
SQL
|
|
201
|
+
node.tail.count{|e| e.comment? }.should == 2
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe "expressionUntilLoop" do
|
|
206
|
+
it "does not consume LOOP token" do
|
|
207
|
+
# parser stops in front of LOOP and dies
|
|
208
|
+
expect{ parse(:expressionUntilLoop, 'abc LOOP') }.to raise_error(Piggly::Parser::Failure)
|
|
209
|
+
|
|
210
|
+
node, rest = parse_some(:expressionUntilLoop, 'abc LOOP xyz')
|
|
211
|
+
rest.should == 'LOOP xyz'
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "cannot be a blank expression" do
|
|
215
|
+
expect{ parse_some(:expressionUntilLoop, ' LOOP') }.to raise_error(Piggly::Parser::Failure)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "cannot be a comment" do
|
|
219
|
+
expect{ parse_some(:expressionUntilLoop, "/* comment */ LOOP") }.to raise_error(Piggly::Parser::Failure)
|
|
220
|
+
expect{ parse_some(:expressionUntilLoop, "-- comment\n LOOP") }.to raise_error(Piggly::Parser::Failure)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "can be a string" do
|
|
224
|
+
node, rest = parse_some(:expressionUntilLoop, "'string' LOOP")
|
|
225
|
+
node.should be_expression
|
|
226
|
+
node.count{|e| e.string? }.should == 1
|
|
227
|
+
|
|
228
|
+
node, rest = parse_some(:expressionUntilLoop, "$$ string $$ LOOP")
|
|
229
|
+
node.should be_expression
|
|
230
|
+
node.count{|e| e.string? }.should == 1
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "can be an arithmetic expression" do
|
|
234
|
+
node, rest = parse_some(:expressionUntilLoop, "10 * (3 + x) LOOP")
|
|
235
|
+
node.should be_expression
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "can be an SQL statement" do
|
|
239
|
+
node, rest = parse_some(:expressionUntilLoop, "SELECT id FROM dataset LOOP")
|
|
240
|
+
node.should be_expression
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "can be an expression with comments embedded" do
|
|
244
|
+
node, rest = parse_some(:expressionUntilLoop, <<-SQL)
|
|
245
|
+
SELECT id -- primary key LOOP
|
|
246
|
+
FROM "dataset" /* LOOP */ -- previous comments shouldn't terminate expression
|
|
247
|
+
WHERE value IS /*NOT*/ NULL LOOP
|
|
248
|
+
SQL
|
|
249
|
+
node.should be_expression
|
|
250
|
+
node.count{|e| e.comment? }.should == 4
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "can be an expression with strings and comments embedded" do
|
|
254
|
+
node, rest = parse_some(:expressionUntilLoop, <<-SQL)
|
|
255
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
256
|
+
FROM dataset /* 2. this one's no problem either */
|
|
257
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
258
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
259
|
+
/* 4. farewell comment in tail */ LOOP
|
|
260
|
+
SQL
|
|
261
|
+
node.should be_expression
|
|
262
|
+
node.count{|e| e.comment? }.should == 4
|
|
263
|
+
node.count{|e| e.string? }.should == 1
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "can be an expression with strings embedded" do
|
|
267
|
+
node, rest = parse_some(:expressionUntilLoop, <<-SQL)
|
|
268
|
+
SELECT id, created_at
|
|
269
|
+
FROM "dataset"
|
|
270
|
+
WHERE value IS NOT NULL
|
|
271
|
+
AND value <> ' LOOP this should not terminate expression'
|
|
272
|
+
AND created_at = '2001-01-01' LOOP
|
|
273
|
+
SQL
|
|
274
|
+
node.should be_expression
|
|
275
|
+
node.count{|e| e.string? }.should == 2
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "combines trailing whitespace into 'tail' node" do
|
|
279
|
+
node, rest = parse_some(:expressionUntilLoop, "a := x + y \tLOOP")
|
|
280
|
+
node.should be_expression
|
|
281
|
+
node.tail.source_text.should == " \t"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it "combines trailing comments into 'tail' node" do
|
|
285
|
+
node, rest = parse_some(:expressionUntilLoop, "a := x + y /* note -- comment */LOOP")
|
|
286
|
+
node.should be_expression
|
|
287
|
+
node.tail.source_text.should == ' /* note -- comment */'
|
|
288
|
+
|
|
289
|
+
node, rest = parse_some(:expressionUntilLoop, <<-SQL)
|
|
290
|
+
SELECT id -- 1. upcoming single quote doesn't matter
|
|
291
|
+
FROM dataset /* 2. this one's no problem either */
|
|
292
|
+
WHERE value LIKE '/* comment within a string! shouldn''t parse a comment */'
|
|
293
|
+
AND length(value) > 10 -- 3. this comment in tail doesn't contain any 'string's
|
|
294
|
+
/* 4. farewell comment in tail */LOOP
|
|
295
|
+
SQL
|
|
296
|
+
node.tail.count{|e| e.comment? }.should == 2
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Piggly
|
|
4
|
+
|
|
5
|
+
describe Parser, "statements" do
|
|
6
|
+
include GrammarHelper
|
|
7
|
+
|
|
8
|
+
describe "assignment statements" do
|
|
9
|
+
it "parse successfully" do
|
|
10
|
+
node = parse(:statement, "a := 10;")
|
|
11
|
+
node.should be_statement
|
|
12
|
+
node.count{|e| e.assignment? }.should == 1
|
|
13
|
+
|
|
14
|
+
node.find{|e| e.named? :lval }.should be_a(Parser::Nodes::Assignable)
|
|
15
|
+
node.find{|e| e.named? :rval }.should be_expression
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "must end with a semicolon" do
|
|
19
|
+
expect { parse_some(:statement, 'a := 10') }.to raise_error(Piggly::Parser::Failure)
|
|
20
|
+
expect { parse(:statement, 'a := 10') }.to raise_error(Piggly::Parser::Failure)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "can use := or =" do
|
|
24
|
+
a = parse(:statement, "a := 10;")
|
|
25
|
+
a.should be_statement
|
|
26
|
+
a.count{|e| e.assignment? }.should == 1
|
|
27
|
+
|
|
28
|
+
b = parse(:statement, "a = 10;")
|
|
29
|
+
b.should be_statement
|
|
30
|
+
b.count{|e| e.assignment? }.should == 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "can assign strings" do
|
|
34
|
+
node = parse(:statement, "a := 'string';")
|
|
35
|
+
rval = node.find{|e| e.named? :rval }
|
|
36
|
+
rval.count{|e| e.string? }.should == 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "can assign value expressions containing comments" do
|
|
40
|
+
['a := /* comment */ 100;',
|
|
41
|
+
'a := 100 /* comment */;',
|
|
42
|
+
'a := 10 /* comment */ + 10;'].test_each do |s|
|
|
43
|
+
node = parse(:statement, s)
|
|
44
|
+
rval = node.find{|e| e.named? :rval }
|
|
45
|
+
rval.count{|e| e.comment? }.should == 1
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "can assign value expressions containing strings" do
|
|
50
|
+
node = parse(:statement, "a := 'Hello,' || space || 'world';")
|
|
51
|
+
rval = node.find{|e| e.named? :rval }
|
|
52
|
+
rval.count{|e| e.string? }.should == 2
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "can assign value expressions containing comments and strings" do
|
|
56
|
+
node = parse(:statement, <<-SQL.strip)
|
|
57
|
+
a := (SELECT fk, count(*), 'not a statement;'
|
|
58
|
+
FROM dataset
|
|
59
|
+
WHERE id > 100 /* filter out 'reserved' range */
|
|
60
|
+
AND id < 900 -- shouldn't be a string
|
|
61
|
+
OR value <> 'alpha /* no comment */;'
|
|
62
|
+
GROUP BY /* pk ; 'abc' */ fk);
|
|
63
|
+
SQL
|
|
64
|
+
rval = node.find{|e| e.named? :rval }
|
|
65
|
+
rval.count{|e| e.string? }.should == 2
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
end
|