each_sql 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,19 @@
1
+ ### 0.3.0 / 2012/03/10
2
+ * Internal implementation revised.
3
+ * At first, I thought this would be trivial,
4
+ that I didn't need a real parser for just breaking SQL scripts
5
+ into individual executable units.
6
+ I couldn't be more wrong. Codes for handling a few exceptional cases
7
+ soon piled up and became unmaintainable.
8
+ The new version now employs Citrus parser for processing SQL scripts.
9
+ The output is not backward-compatible, for instance, comments before and after
10
+ each execution block are trimmed out.
11
+ * Supports PostgreSQL (experimental)
12
+ * `delimiter` command works for all types
13
+
1
14
  ### 0.2.5 / 2011/09/01
2
15
  * Can pass block directly to EachSQL(script)
16
+
3
17
  ```ruby
4
18
  EachSQL(script) do |sql|
5
19
  # ...
data/Gemfile CHANGED
@@ -2,11 +2,13 @@ source "http://rubygems.org"
2
2
  # Add dependencies required to use your gem here.
3
3
  # Example:
4
4
  # gem "activesupport", ">= 2.3.5"
5
+ gem 'citrus', '~> 2.4.1'
6
+ gem 'erubis', '~> 2.7.0'
7
+ gem 'quote_unquote', '~> 0.1.1'
5
8
 
6
9
  # Add dependencies to develop your gem here.
7
10
  # Include everything needed to run rake, tests, features, etc.
8
11
  group :development do
9
12
  gem "bundler", "~> 1.0.0"
10
13
  gem "jeweler", "~> 1.6.2"
11
- gem "rcov", ">= 0"
12
14
  end
data/Gemfile.lock CHANGED
@@ -1,14 +1,15 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ citrus (2.4.1)
5
+ erubis (2.7.0)
4
6
  git (1.2.5)
5
7
  jeweler (1.6.4)
6
8
  bundler (~> 1.0)
7
9
  git (>= 1.2.5)
8
10
  rake
9
- rake (0.9.2)
10
- rcov (0.9.10)
11
- rcov (0.9.10-java)
11
+ quote_unquote (0.1.1)
12
+ rake (0.9.2.2)
12
13
 
13
14
  PLATFORMS
14
15
  java
@@ -16,5 +17,7 @@ PLATFORMS
16
17
 
17
18
  DEPENDENCIES
18
19
  bundler (~> 1.0.0)
20
+ citrus (~> 2.4.1)
21
+ erubis (~> 2.7.0)
19
22
  jeweler (~> 1.6.2)
20
- rcov
23
+ quote_unquote (~> 0.1.1)
data/README.markdown CHANGED
@@ -1,13 +1,16 @@
1
- # each_sql
1
+ each_sql
2
+ ========
2
3
 
3
- Enumerate each SQL statement in the given SQL script.
4
+ Enumerate executable blocks in the given SQL script.
4
5
 
5
- ## Installation
6
+ Installation
7
+ ------------
6
8
  ```
7
9
  gem install 'each_sql'
8
10
  ```
9
11
 
10
- ## Example
12
+ Example
13
+ -------
11
14
  ### Basic
12
15
  ```ruby
13
16
  require 'each_sql'
@@ -35,16 +38,24 @@ end
35
38
  EachSQL(plsql_script, :oracle).each do |sql|
36
39
  # ...
37
40
  end
41
+
42
+ # For PostgreSQL scripts
43
+ EachSQL(plpgsql_script, :postgres).each do |sql|
44
+ # ...
45
+ end
38
46
  ```
39
47
 
40
- ## TODO
41
- - More/better tests.
42
- - pgplsql support.
48
+ TODO
49
+ ----
50
+ - More tests.
51
+ - Support for other RDBMSs
43
52
 
44
- ## Warning
53
+ Warning
54
+ -------
45
55
  Stored procedure handling is at best incomplete. Use it at your own risk.
46
56
 
47
- ## Contributing to each_sql
57
+ Contributing to each_sql
58
+ ------------------------
48
59
 
49
60
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
50
61
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
@@ -54,8 +65,9 @@ Stored procedure handling is at best incomplete. Use it at your own risk.
54
65
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
55
66
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
56
67
 
57
- ## Copyright
68
+ Copyright
69
+ ---------
58
70
 
59
- Copyright (c) 2011 Junegunn Choi. See LICENSE.txt for
71
+ Copyright (c) 2012 Junegunn Choi. See LICENSE.txt for
60
72
  further details.
61
73
 
data/Rakefile CHANGED
@@ -32,14 +32,6 @@ Rake::TestTask.new(:test) do |test|
32
32
  test.verbose = true
33
33
  end
34
34
 
35
- require 'rcov/rcovtask'
36
- Rcov::RcovTask.new do |test|
37
- test.libs << 'test'
38
- test.pattern = 'test/**/test_*.rb'
39
- test.verbose = true
40
- test.rcov_opts << '--exclude "gems/*"'
41
- end
42
-
43
35
  task :default => :test
44
36
 
45
37
  require 'rake/rdoctask'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.5
1
+ 0.3.0
data/each_sql.gemspec CHANGED
@@ -43,16 +43,13 @@ Gem::Specification.new do |s|
43
43
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
44
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
45
45
  s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
46
- s.add_development_dependency(%q<rcov>, [">= 0"])
47
46
  else
48
47
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
49
48
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
50
- s.add_dependency(%q<rcov>, [">= 0"])
51
49
  end
52
50
  else
53
51
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
54
52
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
55
- s.add_dependency(%q<rcov>, [">= 0"])
56
53
  end
57
54
  end
58
55
 
data/lib/each_sql.rb CHANGED
@@ -1,126 +1,62 @@
1
1
  # encoding: UTF-8
2
2
  # Junegunn Choi (junegunn.c@gmail.com)
3
3
 
4
+ require 'rubygems'
4
5
  require 'each_sql/each_sql'
6
+ require 'each_sql/parser'
5
7
 
6
8
  # Shortcut method for creating a Enumerable EachSQL object for the given input.
7
9
  # @param[String] input Input script.
8
10
  # @param[Symbol] The type of the input SQL script. :default, :mysql, and :oracle (or :plsql)
9
- # @return[EachSQL] Enumerable
11
+ # @yield[String] Executable SQL statement or block.
12
+ # @return[Array] Array of executable SQL statements and blocks.
10
13
  def EachSQL input, type = :default
11
- esql = EachSQL.new(input, EachSQL::Defaults[type])
14
+ esql = EachSQL.new(type)
15
+ ret = []
16
+ result = {}
17
+
18
+ process = lambda {
19
+ return if esql.empty?
20
+ result = esql.shift
21
+ sqls = result[:sqls]
22
+ sqls.each do |sql|
23
+ if block_given?
24
+ yield sql
25
+ else
26
+ ret << sql
27
+ end
28
+ end
29
+ }
12
30
 
13
- if block_given?
14
- esql.each do |sql|
15
- yield sql
31
+ input.to_s.each_line do |line|
32
+ case line
33
+ when /^\s*delimiter\s+(\S+)/i
34
+ process.call
35
+ if esql.empty?
36
+ esql.delimiter = $1
37
+ else
38
+ esql << line
39
+ end
40
+ when /#{Regexp.escape esql.delimiter}/
41
+ esql << line
42
+ process.call
43
+ else
44
+ esql << line
16
45
  end
17
- else
18
- esql
19
46
  end
20
- end
21
-
22
- class EachSQL
23
- # EachSQL::Default Hash is a set of pre-defined parsing rules
24
- # - :default: Default parsing rules for vendor-independent SQL scripts
25
- # - :mysql: Parsing rules for MySQL scripts. Understands `delimiter' statements.
26
- # - :oracle: Parsing rules for Oracle scripts. Removes trailing slashes after begin-end blocks.
27
- Defaults = {
28
- :default => {
29
- :delimiter => /;+/,
30
- :blocks => {
31
- /`/ => /`/,
32
- /"/ => /"/,
33
- /'/ => /'/,
34
- /\/\*[^+]/ => /\*\//,
35
- /--+/ => $/,
36
- },
37
- :nesting_blocks => {
38
- /\bdeclare.*?;\s*?begin\b/im => /;\s*?end\b/i,
39
- /\bbegin\b/i => /;\s*?end\b/i,
40
- },
41
- :nesting_context => [
42
- /\A\s*(begin|declare|create\b[^;]+?\b(procedure|function|trigger|package))\b/im
43
- ],
44
- :callbacks => {},
45
- :ignore => [],
46
- :replace => {},
47
- # Let's assume we don't change delimiters within usual sql scripts
48
- :strip_delimiter => lambda { |obj, stmt| stmt.sub(/\A;+/, '').sub(/;+\Z/, '') }
49
- },
50
47
 
51
- :mysql => {
52
- :delimiter => /;+|delimiter\s+\S+/i,
53
- :blocks => {
54
- /`/ => /`/,
55
- /"/ => /"/,
56
- /'/ => /'/,
57
- /\/\*[^+]/ => /\*\//,
58
- /--+/ => $/,
59
- },
60
- :nesting_blocks => {
61
- /\bbegin\b/i => /\bend\b/i
62
- },
63
- :nesting_context => [
64
- /\A\s*(begin|create\b[^;]+?\b(procedure|function|trigger))\b/im
65
- ],
66
- # We need to change delimiter on `delimiter' command
67
- :callbacks => {
68
- /^\s*delimiter\s+(\S+)/i => lambda { |obj, stmt, md|
69
- new_delimiter = Regexp.new(Regexp.escape md[1])
70
- obj.delimiter = /(#{new_delimiter})+|delimiter\s+\S+/i
71
- obj.delimiter_string = md[1]
72
- }
73
- },
74
- :ignore => [
75
- /^delimiter\s+\S+$/i
76
- ],
77
- :replace => {},
78
- :strip_delimiter => lambda { |obj, stmt|
79
- stmt.gsub(/(#{Regexp.escape(obj.delimiter_string || ';')})+\Z/, '')
80
- }
81
- },
48
+ if !esql.empty?
49
+ process.call
50
+ end
82
51
 
83
- :oracle => {
84
- :delimiter => /;+/,
85
- :blocks => {
86
- /`/ => /`/,
87
- /"/ => /"/,
88
- /'/ => /'/,
89
- /\/\*[^+]/ => /\*\//,
90
- /--+/ => $/,
91
- },
92
- :nesting_blocks => {
93
- /\bbegin\b/i => /\bend\b/i,
94
- /\bdeclare.*?;\s*?begin\b/im => {
95
- :closer => %r{;\s*/}m,
96
- # Stops immediately
97
- :pop => true
98
- },
99
- /\bcreate[^;]+?\b(procedure|function|trigger|package)\b/im => {
100
- :closer => %r{;\s*/}m,
101
- # Stops immediately
102
- :pop => true
103
- }
104
- },
105
- :nesting_context => [
106
- /\A\s*(\/\s*)*(begin|declare|create\b[^;]+?\b(procedure|function|trigger|package))\b/im
107
- ],
108
- :callbacks => {
109
- /\Abegin\b/ => lambda { |obj, stmt, md|
110
- # Oracle needs this
111
- stmt << ';' if stmt !~ /;\Z/
112
- }
113
- },
114
- :ignore => [],
115
- :replace => { %r[\A/] => '' },
116
- :strip_delimiter => lambda { |obj, stmt|
117
- stmt.gsub(/(#{stmt =~ /;\s*\// ? '/' : ';'})+\Z/, '')
118
- }
119
- }
120
- }
121
- Defaults[:plsql] = Defaults[:oracle] # alias
52
+ if sql = result[:leftover]
53
+ if block_given?
54
+ yield sql
55
+ else
56
+ ret << sql
57
+ end
58
+ end
122
59
 
123
- # Freeze the Hash
124
- Defaults.freeze
60
+ ret
125
61
  end
126
62
 
@@ -1,173 +1,108 @@
1
- # encoding: UTF-8
2
- # Junegunn Choi (junegunn.c@gmail.com)
1
+ require 'stringio'
3
2
 
4
3
  # Enumerable EachSQL object.
5
4
  class EachSQL
6
5
  include Enumerable
7
6
 
8
- def initialize input, options
9
- raise NotImplementedError.new if options.nil?
10
- # immutables
11
- @org_input = input && input.sub(/\A#{[65279].pack('U*')}/, '') # BOM
12
- @options = options
13
- @blocks = @options[:blocks]
14
- @nblocks = @options[:nesting_blocks]
15
- @all_blocks = @blocks.merge @nblocks
16
- end
17
-
18
- def each
19
- return nil if @org_input.nil? || @org_input.empty?
20
- @input = @org_input.dup
7
+ # @param[Symbol] type RDBMS type: :default|:mysql|:oracle|:postgres
8
+ def initialize type
9
+ @type = type
10
+ @data = ''
11
+ @sqls = []
21
12
 
22
- # Zero out comments and string literals to simplify subsequent parsing
23
- @input_c = zero_out @org_input
24
-
25
- @delimiter = @options[:delimiter]
26
- while @input && @input.length > 0
27
- # Extract a statement
28
- statement = next_statement
13
+ self.delimiter = ';'
14
+ end
29
15
 
30
- # When a non-empty statement is found
31
- statement = @options[:strip_delimiter].call self, statement if @options[:strip_delimiter]
32
- if statement.length > 0
33
- # Apply replacements
34
- @options[:replace].each do |k, v|
35
- statement.gsub!(k, v)
36
- end
37
- statement.strip!
16
+ # @param[String] delim SQL delimiter
17
+ # @return[EachSQL]
18
+ def delimiter= delim
19
+ @delim = delim
20
+ @parser = EachSQL::Parser.parser_for @type, delim
21
+ self
22
+ end
38
23
 
39
- # Process callbacks
40
- @options[:callbacks].each do |pattern, callback|
41
- #md = statement.match pattern
42
- md = zero_out(statement).strip.match pattern
43
- callback.call self, statement, md if md
44
- end
24
+ # @return[String] Current delimiter.
25
+ def delimiter
26
+ @delim
27
+ end
45
28
 
46
- # Ignore
47
- if (@options[:ignore] || []).all? { |ipat| statement !~ ipat } && statement.empty? == false
48
- yield statement
49
- @prev_statement = statement
50
- end
51
- end
29
+ # Appends the given String to the buffer.
30
+ # @param[String] input String to append
31
+ def << input
32
+ if input
33
+ @data << input.sub(/\A#{[65279].pack('U*')}/, '') # BOM (FIXME)
52
34
  end
53
- nil
35
+ self
54
36
  end
55
37
 
56
- # To change delimiter while parsing the input
57
- attr_accessor :delimiter, :delimiter_string
58
-
59
- private
60
- def zero_out input
61
- output = input.dup
62
- idx = 0
63
- # Look for the closest block
64
- while true
65
- block_start, opener_length, opener, closer = @blocks.map { |opener, closer|
66
- md = match output, opener, idx
67
- [md && md[:begin], md && md[:length], opener, closer]
68
- }.reject { |e| e.first.nil? }.min_by(&:first)
69
- break if block_start.nil?
70
-
71
- md = match output, closer, block_start + opener_length
72
- idx = block_end = md ? md[:end] : (output.length-1)
73
-
74
- output[block_start...block_end] = ' ' * (block_end - block_start)
75
- end
76
- output
38
+ # Parses current buffer and returns the result in Hash.
39
+ # :sqls is an Array of processed executable SQL blocks,
40
+ # :leftover is the unparsed trailing data
41
+ # @return [Hash]
42
+ def shift
43
+ result = @parser.parse @data
44
+ @data = result.captures[:leftover].join
45
+ leftover = strip_sql(@data)
46
+ {
47
+ :sqls =>
48
+ result.captures[:execution_block].map { |b| strip_sql b },
49
+ :leftover => leftover.empty? ? nil : leftover
50
+ }
77
51
  end
78
52
 
79
- def next_statement
80
- @cur = 0
53
+ # Return is the buffer is empty
54
+ # @return [Boolean]
55
+ def empty?
56
+ @data.gsub(/\s/, '').empty?
57
+ end
81
58
 
82
- while process_next_block != :done
59
+ # Parses the buffer and enumerates through the executable blocks.
60
+ # @yield [String]
61
+ # @return [NilClass]
62
+ def each
63
+ result = shift
64
+ sqls = (result[:sqls] + result[:leftover]).
65
+ map { |sql| strip_sql(sql) }.
66
+ reject(&:empty?)
67
+ sqls.each do |sql|
68
+ yield sql
83
69
  end
84
-
85
- ret = @input[0...@cur].strip
86
- @input = @input[@cur..-1]
87
- @input_c = @input_c[@cur..-1]
88
- return ret
89
70
  end
90
71
 
91
- def process_next_block expect = nil
92
- # Look for the closest delimiter
93
- md = match @input_c, @delimiter, @cur
94
- delim_start = md ? md[:begin] : @input.length
95
- delim_end = md ? md[:end] : @input.length
96
-
97
- # Look for the closest block depending on the current context
98
- target_blocks =
99
- if @options[:nesting_context].any? {|pat| @input_c.match pat }
100
- @all_blocks
101
- else
102
- @blocks
103
- end
104
-
105
- block_start, body_start, opener, closer = target_blocks.map { |opener, closer|
106
- closer = closer[:closer] if closer.is_a? Hash
107
- md = match @input_c, opener, @cur
108
- [md && md[:begin], md && md[:end], opener, closer]
109
- }.reject { |e| e.first.nil? }.min_by(&:first)
110
-
111
- # If we're nested, look for the parent's closer as well
112
- if expect && (md = match @input_c, expect, @cur) &&
113
- (block_start.nil? || md[:begin] < block_start)
114
-
115
- @cur = md[:end]
116
- return :nest_closer
72
+ private
73
+ def strip_sql sql
74
+ # Preprocess
75
+ case @type
76
+ when :oracle
77
+ sql = sql.sub(/\A[\s\/]+/, '').sub(/[\s\/]+\Z/, '')
117
78
  end
118
79
 
119
- # No block until the next delimiter
120
- if block_start.nil? || block_start > delim_start
121
- @cur = delim_end
122
- return :done
80
+ # FIXME: Infinite loop?
81
+ # sql = sql.gsub(
82
+ # /
83
+ # (?:
84
+ # (?:\A(?:#{Regexp.escape @delim}|[\s]+)+)
85
+ # |
86
+ # (?:(?:#{Regexp.escape @delim}|[\s]+)+\Z)
87
+ # )+
88
+ # /x, '')
89
+ prev_sql = nil
90
+ delim = Regexp.escape @delim
91
+ while prev_sql != sql
92
+ prev_sql = sql
93
+ sql = sql.strip.sub(/\A(?:#{delim})+/, '').sub(/(?:#{delim})+\Z/, '')
123
94
  end
124
95
 
125
- # We found a block. Look for the end of it
126
- @cur = body_start
127
-
128
- # If nesting block, we go deeper
129
- if @nblocks.keys.include? opener
130
- while true
131
- ret = process_next_block(closer)
132
- break if ret == :nest_closer
133
- throw_exception(closer) if @cur >= @input.length - 1
96
+ # Postprocess
97
+ case @type
98
+ when :oracle
99
+ if sql =~ /\bend(\s+\S+)?\Z/i
100
+ sql = sql + ';'
134
101
  end
135
- return :done if @nblocks[opener].is_a?(Hash) && @nblocks[opener][:pop]
136
-
137
- # If non-nesting block, just skip through it
138
- else
139
- skip_through_block closer
140
102
  end
141
103
 
142
- return :continue
104
+ sql
143
105
  end
144
106
 
145
- # For Ruby 1.8 compatibility
146
- def match str, pat, idx
147
- md = str[idx..-1].match(pat)
148
- return nil if md.nil?
149
-
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
157
-
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]
163
- end
164
-
165
- def throw_exception closer
166
- raise ArgumentError.new(
167
- "Unclosed block: was expecting #{closer.inspect} " +
168
- "while processing #{(@input[0, 60] + ' ... ').inspect}" +
169
- (@prev_statement ?
170
- " after #{@prev_statement.inspect}" : ""))
171
- end
172
107
  end#EachSQL
173
108