each_sql 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ === 0.2.0 / 2011/06/17
2
+ * Second release. Handles more cases.
3
+ * Ruby 1.8 compatible
4
+
5
+ === 0.1.0 / 2011/06/15
6
+ * Initial release. Turned out to be very flawed :p
7
+
data/Gemfile.lock CHANGED
@@ -8,8 +8,10 @@ GEM
8
8
  rake
9
9
  rake (0.8.7)
10
10
  rcov (0.9.9)
11
+ rcov (0.9.9-java)
11
12
 
12
13
  PLATFORMS
14
+ java
13
15
  ruby
14
16
 
15
17
  DEPENDENCIES
data/README.rdoc CHANGED
@@ -31,9 +31,7 @@ Enumerate each SQL statement in the given SQL script.
31
31
  - pgplsql support.
32
32
 
33
33
  == Warning
34
- Should be good enough for regular SQLs, but it is not guaranteed to parse stored procedures correctly.
35
- I thought this would be simple when I first started, but actually it wasn't, and the code now is a mess.
36
- Use it at your own risk.
34
+ Stored procedure handling is at best incomplete. Use it at your own risk.
37
35
 
38
36
  == Contributing to each_sql
39
37
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -8,7 +8,7 @@ class EachSQL
8
8
  def initialize input, options
9
9
  raise NotImplementedError.new if options.nil?
10
10
  # immutables
11
- @org_input = input
11
+ @org_input = input.sub(/\A#{[65279].pack('U*')}/, '') # BOM
12
12
  @options = options
13
13
  @blocks = @options[:blocks]
14
14
  @nblocks = @options[:nesting_blocks]
@@ -16,21 +16,17 @@ class EachSQL
16
16
  end
17
17
 
18
18
  def each
19
- @input = @org_input
19
+ # Phase 1: comment out input
20
+ @input = @org_input.dup
21
+ @input_c = zero_out @org_input
22
+
20
23
  return nil if @input.nil? || @input.empty?
21
24
 
22
25
  @delimiter = @options[:delimiter]
23
26
 
24
- reset_cursor
25
- while @input
26
- # First look for next delimiter, this is to reduce the search space for blocks.
27
- extend_scope
28
-
29
- # We're done. Finished. Period. Out!
30
- break if scope.empty?
31
-
27
+ while @input && @input.length > 0
32
28
  # Extract a statement
33
- statement = extract_statement
29
+ statement = next_statement
34
30
 
35
31
  # When a non-empty statement is found
36
32
  statement = @options[:strip_delimiter].call self, statement if @options[:strip_delimiter]
@@ -48,7 +44,8 @@ class EachSQL
48
44
  end
49
45
 
50
46
  # Ignore
51
- if (@options[:ignore] || []).all? { |ipat| statement !~ ipat }
47
+ if statement.length > 0 &&
48
+ (@options[:ignore] || []).all? { |ipat| statement !~ ipat }
52
49
  yield statement
53
50
  @prev_statement = statement
54
51
  end
@@ -59,75 +56,84 @@ class EachSQL
59
56
 
60
57
  attr_accessor :delimiter, :delimiter_string
61
58
  private
59
+ def zero_out input
60
+ output = input.dup
61
+ idx = 0
62
+ # Look for the closest block
63
+ while true
64
+ block_start, opener_length, opener, closer = @blocks.map { |opener, closer|
65
+ md = match output, opener, idx
66
+ [md && md[:begin], md && md[:length], opener, closer]
67
+ }.reject { |e| e.first.nil? }.min_by(&:first)
68
+ break if block_start.nil?
62
69
 
63
- def extract_statement
64
- while process_next_block != :not_found
65
- end
66
-
67
- ret = scope.strip
68
- @input = @input[@next_head..-1]
69
- reset_cursor
70
- return ret
71
- end
72
-
73
- def reset_cursor
74
- @cur = @from = @to = 0
75
- end
70
+ md = match output, closer, block_start + opener_length
71
+ idx = block_end = md ? md[:end] : (output.length-1)
76
72
 
77
- def scope
78
- @to ? @input[0, @to] : @input
73
+ output[block_start...block_end] = ' ' * (block_end - block_start)
74
+ end
75
+ output
79
76
  end
80
77
 
81
- def extend_scope
82
- md = @input.match @delimiter, @to
78
+ def next_statement
79
+ @cur = 0
83
80
 
84
- if md
85
- @to = md.begin(0) + md[0].length
86
- @next_head = @to
87
- else
88
- @to = @input.length
89
- @next_head = @input.length
81
+ while process_next_block != :done
90
82
  end
91
- #puts "Extended: #{scope.inspect} #{@input[@next_head..-1]}"
83
+
84
+ ret = @input[0...@cur].strip
85
+ @input = @input[@cur..-1]
86
+ @input_c = @input_c[@cur..-1]
87
+ return ret
92
88
  end
93
89
 
94
90
  def process_next_block expect = nil
91
+ # Look for the closest delimiter
92
+ md = match @input_c, @delimiter, @cur
93
+ delim_start = md ? md[:begin] : @input.length
94
+ delim_end = md ? md[:end] : @input.length
95
+
95
96
  # Look for the closest block
96
- block_start, opener_length, opener, closer = @all_blocks.map { |opener, closer|
97
+ target_blocks =
98
+ if @options[:nesting_context].any? {|pat| @input_c.match pat }
99
+ @all_blocks
100
+ else
101
+ @blocks
102
+ end
103
+
104
+ block_start, body_start, opener, closer = target_blocks.map { |opener, closer|
97
105
  closer = closer[:closer] if closer.is_a? Hash
98
- md = scope.match(opener, @cur)
99
- [md && md.begin(0), md && md[0].length, opener, closer]
106
+ md = match @input_c, opener, @cur
107
+ [md && md[:begin], md && md[:end], opener, closer]
100
108
  }.reject { |e| e.first.nil? }.min_by(&:first)
101
109
 
102
- # p [scope, scope[@cur..-1], expect, scope.index(expect, @cur), block_start] if expect
103
- # We found a block, but after the end of the nesting block
104
- if expect &&
105
- (prev_end = scope.index(expect, @cur)) &&
106
- (block_start.nil? || prev_end <= block_start)
107
- skip_through_block expect, prev_end == block_start
108
- return :end_nest
110
+ # If we're nested, look for the parent's closer as well
111
+ if expect && (md = match @input_c, expect, @cur) &&
112
+ (block_start.nil? || md[:begin] < block_start)
113
+
114
+ @cur = md[:end]
115
+ return :nest_closer
109
116
  end
110
117
 
111
- # If no block in this scope
112
- return :not_found if block_start.nil?
118
+ # No block until the next delimiter
119
+ if block_start.nil? || block_start > delim_start
120
+ @cur = delim_end
121
+ return :done
122
+ end
123
+
124
+ # #####################################
113
125
 
114
126
  # We found a block. Look for the end of it
115
- @cur = block_start + opener_length
127
+ @cur = body_start
116
128
 
117
129
  # If nesting block, we go deeper
118
130
  if @nblocks.keys.include? opener
119
- @prev_delimiter = @delimiter
120
- if @nblocks[opener].is_a? Hash
121
- @delimiter = @nblocks[opener][:delimiter] || @delimiter
122
- end
123
131
  while true
124
132
  ret = process_next_block(closer)
125
-
126
- break if ret == :end_nest
127
- extend_scope if ret == :not_found
128
- throw_exception(closer) if scope.length == @input.length
133
+ break if ret == :nest_closer
134
+ throw_exception(closer) if @cur >= @input.length - 1
129
135
  end
130
- @delimiter = @prev_delimiter
136
+ return :done if @nblocks[opener].is_a?(Hash) && @nblocks[opener][:pop]
131
137
 
132
138
  # If non-nesting block, just skip through it
133
139
  else
@@ -137,22 +143,29 @@ private
137
143
  return :continue
138
144
  end
139
145
 
140
- def skip_through_block closer, rewind_delimiter = false
141
- md = @input.match closer, @cur
142
- block_end = md && md.begin(0)
146
+ def match str, pat, idx
147
+ md = str[idx..-1].match(pat)
148
+ return nil if md.nil?
143
149
 
144
- throw_exception(closer) if block_end.nil?
150
+ result = {
151
+ :begin => md && (md.begin(0) + idx),
152
+ :length => md && md[0].length,
153
+ :end => md && (md.end(0) + idx)
154
+ }
155
+ result
156
+ end
145
157
 
146
- @cur = block_end + (rewind_delimiter ? 0 : md[0].length)
147
- while @cur > scope.length
148
- extend_scope
149
- end
158
+ def skip_through_block closer
159
+ md = match @input_c, closer, @cur
160
+ throw_exception(closer) if md.nil?
161
+
162
+ @cur = md[:end]
150
163
  end
151
164
 
152
165
  def throw_exception closer
153
166
  raise ArgumentError.new(
154
167
  "Unclosed block: was expecting #{closer.inspect} " +
155
- "while processing #{$/ + scope.inspect}" +
168
+ "while processing #{(@input[0, 60] + ' ... ').inspect}" +
156
169
  (@prev_statement ?
157
170
  " after #{@prev_statement.inspect}" : ""))
158
171
  end
data/lib/each_sql.rb CHANGED
@@ -20,14 +20,19 @@ class EachSQL
20
20
  :default => {
21
21
  :delimiter => /;+/,
22
22
  :blocks => {
23
+ /`/ => /`/,
24
+ /"/ => /"/,
23
25
  /'/ => /'/,
24
26
  /\/\*[^+]/ => /\*\//,
25
27
  /--+/ => $/,
26
28
  },
27
29
  :nesting_blocks => {
28
- /\bdeclare\b/i => /\bbegin\b/i,
29
- /\bbegin\b/i => /\bend\b/i
30
+ /\bdeclare.*?;\s*?begin\b/im => /;\s*?end\b/i,
31
+ /\bbegin\b/i => /;\s*?end\b/i,
30
32
  },
33
+ :nesting_context => [
34
+ /\A\s*(begin|declare|create\b[^;]+?\b(procedure|function|trigger|package))\b/im
35
+ ],
31
36
  :callbacks => {},
32
37
  :ignore => [],
33
38
  :replace => {},
@@ -38,6 +43,8 @@ class EachSQL
38
43
  :mysql => {
39
44
  :delimiter => /;+|delimiter\s+\S+/i,
40
45
  :blocks => {
46
+ /`/ => /`/,
47
+ /"/ => /"/,
41
48
  /'/ => /'/,
42
49
  /\/\*[^+]/ => /\*\//,
43
50
  /--+/ => $/,
@@ -45,11 +52,14 @@ class EachSQL
45
52
  :nesting_blocks => {
46
53
  /\bbegin\b/i => /\bend\b/i
47
54
  },
55
+ :nesting_context => [
56
+ /\A\s*(begin|create\b[^;]+?\b(procedure|function|trigger))\b/im
57
+ ],
48
58
  # We need to change delimiter on `delimiter' command
49
59
  :callbacks => {
50
60
  /^\s*delimiter\s+(\S+)/i => lambda { |obj, stmt, md|
51
61
  new_delimiter = Regexp.new(Regexp.escape md[1])
52
- obj.delimiter = /#{new_delimiter}+|delimiter\s+\S+/i
62
+ obj.delimiter = /(#{new_delimiter})+|delimiter\s+\S+/i
53
63
  obj.delimiter_string = md[1]
54
64
  }
55
65
  },
@@ -58,29 +68,45 @@ class EachSQL
58
68
  ],
59
69
  :replace => {},
60
70
  :strip_delimiter => lambda { |obj, stmt|
61
- stmt.chomp(obj.delimiter_string || ';')
71
+ stmt.gsub(/(#{Regexp.escape(obj.delimiter_string || ';')})+\Z/, '')
62
72
  }
63
73
  },
64
74
 
65
75
  :oracle => {
66
76
  :delimiter => /;+/,
67
77
  :blocks => {
78
+ /`/ => /`/,
79
+ /"/ => /"/,
68
80
  /'/ => /'/,
69
81
  /\/\*[^+]/ => /\*\//,
70
82
  /--+/ => $/,
71
83
  },
72
84
  :nesting_blocks => {
73
85
  /\bbegin\b/i => /\bend\b/i,
74
- /\bcreate[^;]*\b(procedure|function|trigger|package)\b/im => {
86
+ /\bdeclare.*?;\s*?begin\b/im => {
75
87
  :closer => %r{;\s*/}m,
76
- :delimiter => /;\s*\//
88
+ # Stops immediately
89
+ :pop => true
90
+ },
91
+ /\bcreate[^;]+?\b(procedure|function|trigger|package)\b/im => {
92
+ :closer => %r{;\s*/}m,
93
+ # Stops immediately
94
+ :pop => true
95
+ }
96
+ },
97
+ :nesting_context => [
98
+ /\A\s*(begin|declare|create\b[^;]+?\b(procedure|function|trigger|package))\b/im
99
+ ],
100
+ :callbacks => {
101
+ /\Abegin\b/ => lambda { |obj, stmt, md|
102
+ # Oracle needs this
103
+ stmt << ';' if stmt !~ /;\Z/
77
104
  }
78
105
  },
79
- :callbacks => {},
80
106
  :ignore => [],
81
- :replace => {},
107
+ :replace => { %r[\A/] => '' },
82
108
  :strip_delimiter => lambda { |obj, stmt| obj
83
- stmt.chomp( stmt =~ /;\s*\// ? '/' : ';' )
109
+ stmt.gsub(/(#{stmt =~ /;\s*\// ? '/' : ';'})+\Z/, '')
84
110
  }
85
111
  }
86
112
  }
@@ -1,24 +1,22 @@
1
+ # encoding: UTF-8
2
+
1
3
  $LOAD_PATH << File.dirname(__FILE__)
2
4
  require 'helper'
3
5
 
4
6
  class TestEachSql < Test::Unit::TestCase
5
7
  def setup
6
8
  @sql = [
7
- "select * from a",
8
- "select
9
- *
10
- from
11
- b",
12
- "select 'abc', 'abc;', 'abc''', 'abc/*', 'abc--' from c",
13
-
14
- "select
15
- /*+ help */ *
16
- from
17
- d",
18
- "select * from /* block comment ; */ e",
19
- "select *
20
- from -- line comment ; /* ;; */
21
- f",
9
+ "-------------- begin-end block;
10
+ declare
11
+ /* end; */
12
+ /* begin */
13
+ null;
14
+ null;
15
+ null;
16
+ begin
17
+ /* end */
18
+ null;
19
+ end",
22
20
  "-------------- begin-end block;
23
21
  begin
24
22
  -- begin-end block;
@@ -34,16 +32,21 @@ begin
34
32
  -- end
35
33
  /* end */
36
34
  end",
37
- "-------------- begin-end block;
38
- declare
39
- /* end; */
40
- /* begin */
41
- null;
42
- null;
43
- null;
44
- begin
45
- /* end */
46
- end",
35
+ "select * from a",
36
+ "select
37
+ *
38
+ from
39
+ b",
40
+ "select 'abc', 'abc;', 'abc''', 'abc/*', 'abc--' from c",
41
+
42
+ "select
43
+ /*+ help */ *
44
+ from
45
+ d",
46
+ "select * from /* block comment ; */ e",
47
+ "select *
48
+ from -- line comment ; /* ;; */
49
+ f",
47
50
  "-------------- begin-end block;
48
51
  declare
49
52
  /* end; */
@@ -66,13 +69,79 @@ begin
66
69
  /* end */
67
70
  end",
68
71
  "select * from dual",
69
- "select * from dual"]
72
+ "select b `begin` from dual",
73
+ 'select b "begin" from dual',
74
+ 'select
75
+ begin , begin.* from begin'
76
+ ]
70
77
 
71
- @oracle = "
78
+ @oracle = [
79
+ 'select * from dual',
80
+ 'create /* procedure */ sequence a',
81
+ "create package something as
82
+ procedure log;
83
+ procedure log;
84
+ procedure log;
85
+ end something;",
86
+ "Create or replace Procedure tmmp(p1 number default 'begin', p2 number) as
87
+ str number(8, 2) := 1 / 4;
88
+ begin
89
+ 1 / 2;
90
+ begin
91
+ 1 / 4;
92
+ null;
93
+ end;
94
+ exception
95
+ when others then
96
+ raise;
97
+ end;",
98
+ "declare
99
+ a int;
100
+ begin
101
+ 1 / 2;
102
+ begin
103
+ 1 / 4;
104
+ null;
105
+ end;
106
+ exception
107
+ when others then
108
+ raise;
109
+ end;",
110
+ "begin
111
+ null;
112
+ end;",
113
+ "select * from dual",
114
+ "select begin, end, create, procedure, end, from dual",
115
+ "select * from dual"
116
+ ]
117
+
118
+ @oracle_script = "
72
119
  select * from dual;
73
- Create or replace Procedure tmmp(p1 number, p2 number) as
120
+ create /* procedure */ sequence a;
121
+ create package something as
122
+ procedure log;
123
+ procedure log;
124
+ procedure log;
125
+ end something;
126
+ /
127
+
128
+ Create or replace Procedure tmmp(p1 number default 'begin', p2 number) as
74
129
  str number(8, 2) := 1 / 4;
75
130
  begin
131
+ 1 / 2;
132
+ begin
133
+ 1 / 4;
134
+ null;
135
+ end;
136
+ exception
137
+ when others then
138
+ raise;
139
+ end;
140
+ /
141
+ declare
142
+ a int;
143
+ begin
144
+ 1 / 2;
76
145
  begin
77
146
  1 / 4;
78
147
  null;
@@ -82,15 +151,53 @@ exception
82
151
  raise;
83
152
  end;
84
153
  /
85
- select * from dual;"
154
+ begin
155
+ null;
156
+ end;
157
+ /
158
+ select * from dual;
159
+ ;
160
+ ;
161
+ ;
86
162
 
87
- @mysql = "
163
+
164
+ ;;;;;;
165
+ ;
166
+
167
+ select begin, end, create, procedure, end, from dual;
168
+ select * from dual;
169
+ "
170
+
171
+ @mysql = [
172
+ "drop procedure if exists proc",
173
+ "create procedure proc(p1 int, p2 int)
174
+ begin
175
+ null;
176
+ begin
177
+ null;
178
+ end;
179
+ end",
180
+ "drop procedure if exists proc2",
181
+ "create procedure proc(p1 int, p2 int)
182
+ begin
183
+ null;
184
+
185
+ end",
186
+ "select * from dual",
187
+ "select b `begin` from dual",
188
+ 'select b "begin" from dual',
189
+ 'select
190
+ begin , begin.* from begin'
191
+ ]
192
+ @mysql_script = "
88
193
  delimiter //
89
194
  drop procedure if exists proc //
90
195
  create procedure proc(p1 int, p2 int)
91
196
  begin
92
197
  null;
93
-
198
+ begin
199
+ null;
200
+ end;
94
201
  end //
95
202
  delimiter ;
96
203
 
@@ -103,7 +210,11 @@ begin
103
210
  end $$
104
211
  delimiter ;
105
212
 
106
- select * from dual;"
213
+ select * from dual;
214
+ select b `begin` from dual;
215
+ select b \"begin\" from dual;
216
+ select
217
+ begin , begin.* from begin"
107
218
  end
108
219
 
109
220
  def test_sql
@@ -117,16 +228,18 @@ select * from dual;"
117
228
  end
118
229
 
119
230
  def test_oracle
120
- EachSQL(@oracle, :oracle).each_with_index do |sql,idx|
231
+ EachSQL(@oracle_script, :oracle).each_with_index do |sql,idx|
121
232
  puts sql
122
233
  puts '-' * 40
234
+ assert_equal @oracle[idx], sql
123
235
  end
124
236
  end
125
237
 
126
238
  def test_mysql
127
- EachSQL(@mysql, :mysql).each_with_index do |sql,idx|
239
+ EachSQL(@mysql_script, :mysql).each_with_index do |sql,idx|
128
240
  puts sql
129
241
  puts '-' * 40
242
+ assert_equal @mysql[idx], sql
130
243
  end
131
244
  end
132
245
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: each_sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-06-15 00:00:00.000000000Z
12
+ date: 2011-06-16 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: &2161738720 !ruby/object:Gem::Requirement
16
+ requirement: &2161248000 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.0.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2161738720
24
+ version_requirements: *2161248000
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: jeweler
27
- requirement: &2161738240 !ruby/object:Gem::Requirement
27
+ requirement: &2161247520 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 1.6.2
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *2161738240
35
+ version_requirements: *2161247520
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rcov
38
- requirement: &2161737760 !ruby/object:Gem::Requirement
38
+ requirement: &2161247040 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *2161737760
46
+ version_requirements: *2161247040
47
47
  description: Enumerate through each SQL statement in SQL scripts.
48
48
  email: junegunn.c@gmail.com
49
49
  executables: []
@@ -53,6 +53,7 @@ extra_rdoc_files:
53
53
  - README.rdoc
54
54
  files:
55
55
  - .document
56
+ - CHANGELOG.rdoc
56
57
  - Gemfile
57
58
  - Gemfile.lock
58
59
  - LICENSE.txt
@@ -79,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
80
  version: '0'
80
81
  segments:
81
82
  - 0
82
- hash: -4303771573274782197
83
+ hash: 2241318967940806787
83
84
  required_rubygems_version: !ruby/object:Gem::Requirement
84
85
  none: false
85
86
  requirements: