vorax 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +30 -0
- data/lib/vorax/base_funnel.rb +30 -0
- data/lib/vorax/output/html_convertor.rb +120 -0
- data/lib/vorax/output/html_funnel.rb +79 -0
- data/lib/vorax/output/pagezip_convertor.rb +20 -0
- data/lib/vorax/output/tablezip_convertor.rb +22 -0
- data/lib/vorax/output/vertical_convertor.rb +53 -0
- data/lib/vorax/output/zip_convertor.rb +117 -0
- data/lib/vorax/parser/argument.rb~ +125 -0
- data/lib/vorax/parser/body_split.rb +168 -0
- data/lib/vorax/parser/conn_string.rb +104 -0
- data/lib/vorax/parser/grammars/alias.rb +912 -0
- data/lib/vorax/parser/grammars/alias.rl +146 -0
- data/lib/vorax/parser/grammars/column.rb +454 -0
- data/lib/vorax/parser/grammars/column.rl +64 -0
- data/lib/vorax/parser/grammars/common.rl +98 -0
- data/lib/vorax/parser/grammars/package_spec.rb +1186 -0
- data/lib/vorax/parser/grammars/package_spec.rl +78 -0
- data/lib/vorax/parser/grammars/plsql_def.rb +469 -0
- data/lib/vorax/parser/grammars/plsql_def.rl +59 -0
- data/lib/vorax/parser/grammars/statement.rb +925 -0
- data/lib/vorax/parser/grammars/statement.rl +83 -0
- data/lib/vorax/parser/parser.rb +320 -0
- data/lib/vorax/parser/plsql_structure.rb +158 -0
- data/lib/vorax/parser/plsql_walker.rb +143 -0
- data/lib/vorax/parser/statement_inspector.rb~ +52 -0
- data/lib/vorax/parser/stmt_inspector.rb +78 -0
- data/lib/vorax/parser/target_ref.rb +110 -0
- data/lib/vorax/sqlplus.rb +281 -0
- data/lib/vorax/version.rb +7 -0
- data/lib/vorax/vorax_io.rb +70 -0
- data/lib/vorax.rb +60 -0
- data/spec/column_spec.rb +40 -0
- data/spec/conn_string_spec.rb +53 -0
- data/spec/package_spec_spec.rb +48 -0
- data/spec/pagezip_spec.rb +153 -0
- data/spec/parser_spec.rb +299 -0
- data/spec/plsql_structure_spec.rb +44 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/sql/create_objects.sql +69 -0
- data/spec/sql/dbms_crypto.spc +339 -0
- data/spec/sql/dbms_crypto.~spc +339 -0
- data/spec/sql/dbms_stats.spc +4097 -0
- data/spec/sql/drop_user.sql +10 -0
- data/spec/sql/muci.spc +24 -0
- data/spec/sql/setup_user.sql +22 -0
- data/spec/sql/test.pkg +67 -0
- data/spec/sqlplus_spec.rb +52 -0
- data/spec/stmt_inspector_spec.rb +84 -0
- data/spec/tablezip_spec.rb +111 -0
- data/spec/vertical_spec.rb +150 -0
- data/vorax.gemspec +21 -0
- metadata +139 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Vorax
|
4
|
+
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
class StatementInspector
|
8
|
+
|
9
|
+
def initialize(statement)
|
10
|
+
@statement = statement
|
11
|
+
end
|
12
|
+
|
13
|
+
def type
|
14
|
+
@type ||= Statement.new.type(statement)
|
15
|
+
end
|
16
|
+
|
17
|
+
def data_source
|
18
|
+
descriptor.refs
|
19
|
+
end
|
20
|
+
|
21
|
+
def recursive_data_source(refs, collect, position)
|
22
|
+
inner = refs.find { |r| r.respond_to?(:range) && r.range.include?(position) }
|
23
|
+
collect.unshift(refs).flatten!
|
24
|
+
if inner
|
25
|
+
desc = Parser::Alias.new
|
26
|
+
desc.walk(inner.base)
|
27
|
+
data_source(desc.refs, collect, position - inner_expr.range.first)
|
28
|
+
else
|
29
|
+
return collect
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def query_fields
|
34
|
+
@query_fields ||= Column.new.walk(descriptor.query_fields)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def descriptor
|
40
|
+
unless @desc
|
41
|
+
@desc = Parser::Alias.new
|
42
|
+
@desc.walk(@statement)
|
43
|
+
end
|
44
|
+
@desc
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Vorax
|
4
|
+
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# A class used to gather metadata information for an SQL statement. This is needed
|
8
|
+
# especially for implementing VoraX code completion.
|
9
|
+
class StmtInspector
|
10
|
+
|
11
|
+
# Creates a new statement inspector.
|
12
|
+
#
|
13
|
+
# @param statement [String] the statement to be inspected
|
14
|
+
def initialize(statement)
|
15
|
+
@statement = statement
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get the type of the statement
|
19
|
+
#
|
20
|
+
# (see Parser.statement_type)
|
21
|
+
def type
|
22
|
+
@type ||= Parser.statement_type(statement)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get all tableref/exprref for this statement, taking into account
|
26
|
+
# the current position within that statement.
|
27
|
+
#
|
28
|
+
# @param position [int] the current position.
|
29
|
+
# @return an array of TableRef/ExprRef objects
|
30
|
+
def data_source(position=0)
|
31
|
+
recursive_data_source(descriptor.refs, position)
|
32
|
+
end
|
33
|
+
|
34
|
+
# If it's a query, the corresponding columns are returned.
|
35
|
+
#
|
36
|
+
# @return an array of columns
|
37
|
+
def query_fields
|
38
|
+
@query_fields ||= Column.new.walk(descriptor.query_fields)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Find the provided alias for the statement, taking into account the
|
42
|
+
# current position.
|
43
|
+
#
|
44
|
+
# @param name [String] the alias name
|
45
|
+
# @param position [int] the current position
|
46
|
+
def find_alias(name, position=0)
|
47
|
+
data_source(position).find { |r| r.pointer && r.pointer.upcase == name.upcase }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def descriptor
|
53
|
+
unless @desc
|
54
|
+
@desc = Parser::Alias.new
|
55
|
+
@desc.walk(@statement)
|
56
|
+
end
|
57
|
+
@desc
|
58
|
+
end
|
59
|
+
|
60
|
+
def recursive_data_source(refs, position, collect = [])
|
61
|
+
inner = refs.find { |r| r.respond_to?(:range) && r.range.include?(position) }
|
62
|
+
collect.unshift(refs).flatten!
|
63
|
+
if inner
|
64
|
+
desc = Parser::Alias.new
|
65
|
+
desc.walk(inner.base)
|
66
|
+
recursive_data_source(desc.refs, position - inner.range.first, collect)
|
67
|
+
else
|
68
|
+
return collect
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vorax
|
4
|
+
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# An abstraction for a table reference within an SQL statement. This class is used
|
8
|
+
# by the StmtInspector to model the FROM clause of a query or the target table of
|
9
|
+
# an INSERT or UPDATE.
|
10
|
+
class TableRef
|
11
|
+
|
12
|
+
attr_reader :base, :pointer
|
13
|
+
|
14
|
+
# Creates a new TableRef object.
|
15
|
+
#
|
16
|
+
# @param base [String] is the actual table/view name from the SQL statement.
|
17
|
+
# @param pointer [String] is the alias of the table, if there's any
|
18
|
+
def initialize(base, pointer = nil)
|
19
|
+
@base = base
|
20
|
+
@pointer = pointer
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the columns of this table ref.
|
24
|
+
#
|
25
|
+
# @return the columns of the table, in an unexpanded form (e.g. tbl.*)
|
26
|
+
def columns
|
27
|
+
["#{@base}.*"]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Compare too table ref objects.
|
31
|
+
#
|
32
|
+
# param obj [TableRef] the other tableref used for comparison.
|
33
|
+
def ==(obj)
|
34
|
+
self.base == obj.base && self.pointer == obj.pointer
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
# This class is used to model a reference within a SQL statement, given as an
|
40
|
+
# expression (e.g. "select * from (select * from dual);")
|
41
|
+
class ExprRef
|
42
|
+
|
43
|
+
attr_reader :base, :range, :pointer
|
44
|
+
|
45
|
+
# Creates a new ExprRef object.
|
46
|
+
#
|
47
|
+
# @param base [String] the actual expresion
|
48
|
+
# @param range [Range] the bounderies of this expression within the parent statement
|
49
|
+
# @param pointer [String] the alias of the expresion, if there is any
|
50
|
+
def initialize(base, range, pointer = nil)
|
51
|
+
@base = base
|
52
|
+
@range = range
|
53
|
+
@pointer = pointer
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get all columns for this expression.
|
57
|
+
#
|
58
|
+
# @return all columns of the query expression
|
59
|
+
def columns
|
60
|
+
collect = []
|
61
|
+
recursive_columns(@base, collect)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Compare too table ref objects.
|
65
|
+
#
|
66
|
+
# param obj [TableRef] the other tableref used for comparison.
|
67
|
+
def ==(obj)
|
68
|
+
self.base == obj.base && self.range == obj.range && self.pointer == obj.pointer
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def recursive_columns(statement, collect)
|
74
|
+
inspector = StmtInspector.new(statement)
|
75
|
+
columns_data = inspector.query_fields
|
76
|
+
columns_data.each do |column|
|
77
|
+
if column =~ /([a-z0-9#$_]+\.)?\*/i
|
78
|
+
#might be an alias
|
79
|
+
alias_name = column[/[a-z0-9#$_]+/]
|
80
|
+
ds = []
|
81
|
+
if alias_name
|
82
|
+
src = inspector.data_source.find do |r|
|
83
|
+
(r.pointer && r.pointer.upcase == alias_name.upcase) || (r.base.upcase == alias_name.upcase)
|
84
|
+
end
|
85
|
+
ds << src if src
|
86
|
+
elsif column == '*'
|
87
|
+
ds = inspector.data_source
|
88
|
+
end
|
89
|
+
if ds.size > 0
|
90
|
+
ds.each do |source|
|
91
|
+
if source.respond_to?(:range)
|
92
|
+
recursive_columns(source.base, collect)
|
93
|
+
else
|
94
|
+
collect << "#{source.base}.*"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
else
|
99
|
+
collect << column
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return collect
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vorax
|
4
|
+
|
5
|
+
# Provides integration with Oracle SqlPlus CLI tool.
|
6
|
+
class Sqlplus
|
7
|
+
|
8
|
+
# the tail size of the output chunk to be searched
|
9
|
+
# for the output END_MARKER.
|
10
|
+
TAIL_LENGTH = 100 unless defined?(TAIL_LENGTH)
|
11
|
+
private_constant :TAIL_LENGTH
|
12
|
+
|
13
|
+
attr_reader :bin_file, :default_funnel_name, :process
|
14
|
+
|
15
|
+
# Creates a new sqlplus instance.
|
16
|
+
#
|
17
|
+
# @param bin_file [String] the path to the SqlPlus executable. By
|
18
|
+
# default is "sqlplus", which requires that the executable to be
|
19
|
+
# in $PATH.
|
20
|
+
def initialize(bin_file = "sqlplus")
|
21
|
+
@bin_file = bin_file
|
22
|
+
@busy = false
|
23
|
+
@start_marker, @end_marker, @cancel_marker = [2.chr, 3.chr, 4.chr]
|
24
|
+
@process = ChildProcess.build(@bin_file, "/nolog")
|
25
|
+
# On Unix we may abort the currently executing query by sending a
|
26
|
+
# INT signal to the Sqlplus process, but we need access to the
|
27
|
+
# send_term private method.
|
28
|
+
class << @process; public :send_signal; end if ChildProcess.unix?
|
29
|
+
@process.duplex = true
|
30
|
+
@process.detach = true
|
31
|
+
@process.io.inherit!
|
32
|
+
@io_read, @io_write = VoraxIO.pipe
|
33
|
+
@process.io.stdout = @io_write
|
34
|
+
@process.start
|
35
|
+
@process.io.stdin.sync = true
|
36
|
+
@current_funnel = nil
|
37
|
+
@default_convertor_name = nil
|
38
|
+
@registered_convertors = {:vertical => Output::VerticalConvertor,
|
39
|
+
:pagezip => Output::PagezipConvertor,
|
40
|
+
:tablezip => Output::TablezipConvertor}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the default convertor for the output returned by sqlplus.
|
44
|
+
#
|
45
|
+
# @param convertor_name [Symbol] the default funnel name. The
|
46
|
+
# valid values are: :vertical, :pagezip and :tablezip
|
47
|
+
def default_convertor=(convertor_name)
|
48
|
+
Vorax.debug("default_convertor=#{convertor_name.inspect}")
|
49
|
+
@default_convertor_name = convertor_name
|
50
|
+
end
|
51
|
+
|
52
|
+
# Register a new convertor for the sqlplus output.
|
53
|
+
#
|
54
|
+
# @param convertor_name [Symbol] the name of the convertor (key)
|
55
|
+
# @param convertor_class [Class] the class which implements the
|
56
|
+
# convertor. It must be a subclass of BaseConvertor.
|
57
|
+
def register_convertor(convertor_name, convertor_class)
|
58
|
+
@registered_convertors[convertor_name] = convertor_class
|
59
|
+
end
|
60
|
+
|
61
|
+
# Execute an sqlplus command.
|
62
|
+
#
|
63
|
+
# @param command [String] the command to be executed.
|
64
|
+
# @param params [Hash] additional parameters. You may use
|
65
|
+
# the following options:
|
66
|
+
# :prep => a string with commands to be executed just before
|
67
|
+
# running the provided command. For example, you may
|
68
|
+
# choose to set some sqlplus options.
|
69
|
+
# :post => a string with commands to be executed after the
|
70
|
+
# provided command was run. Here it's a good place
|
71
|
+
# to put commands that restores some options affected
|
72
|
+
# by the executed command.
|
73
|
+
# :convertor => the convertor used to convert the output received
|
74
|
+
# from Sqlplus. By default it's the convertor set with
|
75
|
+
# "default_convertor=" method.
|
76
|
+
# :pack_file => the file name into which the command(s) to be
|
77
|
+
# executed are wrapped into and then sent for
|
78
|
+
# execution to sqlplus using '@<file_name>'.
|
79
|
+
# Providing this option may prove to be a good
|
80
|
+
# thing for big commands. If this parameter is
|
81
|
+
# not provided then the command is sent directly
|
82
|
+
# to the input IO of the sqlplus process.
|
83
|
+
def exec(command, params = {})
|
84
|
+
Vorax.debug("exec: command=#{command.inspect} params=#{params.inspect}")
|
85
|
+
raise AnotherExecRunning if busy?
|
86
|
+
@tail = ""
|
87
|
+
opts = {
|
88
|
+
:prep => nil,
|
89
|
+
:post => nil,
|
90
|
+
:convertor => @default_convertor_name,
|
91
|
+
:pack_file => nil,
|
92
|
+
}.merge(params)
|
93
|
+
@busy = true
|
94
|
+
@look_for = @start_marker
|
95
|
+
prepare_funnel(opts[:convertor])
|
96
|
+
if @current_funnel && @current_funnel.is_a?(Output::HTMLFunnel)
|
97
|
+
# all HTML funnels expects html format
|
98
|
+
send_text("set markup html on\n")
|
99
|
+
else
|
100
|
+
send_text("set markup html off\n")
|
101
|
+
end
|
102
|
+
if opts[:pack_file]
|
103
|
+
send_text("@#{pack(command, opts)}\n")
|
104
|
+
else
|
105
|
+
send_text("#{opts[:prep]}\n") if opts[:prep]
|
106
|
+
capture { send_text("#{command}\n") }
|
107
|
+
send_text("#{opts[:post]}\n") if opts[:post]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Send a text directly to the stdin of the sqlplus process.
|
112
|
+
#
|
113
|
+
# @param text [String] the text to be sent to sqlplus
|
114
|
+
def send_text(text)
|
115
|
+
@process.io.stdin.print(text)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check if the sqlplus process is busy executing something.
|
119
|
+
#
|
120
|
+
# @return true if the sqlplus is busy executing something,
|
121
|
+
# false otherwise
|
122
|
+
def busy?
|
123
|
+
@busy
|
124
|
+
end
|
125
|
+
|
126
|
+
# Check if the output of a previous executed sqlpus command
|
127
|
+
# was completely fetched out.
|
128
|
+
#
|
129
|
+
# @return true if the whole output was fetched, false otherwise
|
130
|
+
def eof?
|
131
|
+
not busy?
|
132
|
+
end
|
133
|
+
|
134
|
+
# Read the output spit by sqlplus process. If there is any default
|
135
|
+
# convertor, the returned output will be formatted according to
|
136
|
+
# that convertor.
|
137
|
+
#
|
138
|
+
# @param bytes [int] the maximum output chunk size
|
139
|
+
# @return the output chunk
|
140
|
+
def read_output(bytes=4086)
|
141
|
+
output = ""
|
142
|
+
raw_output = @tail
|
143
|
+
begin
|
144
|
+
raw_output << (@io_read.read_nonblock(bytes) || '').gsub(/\r/, '')
|
145
|
+
rescue Errno::EAGAIN
|
146
|
+
end
|
147
|
+
if raw_output
|
148
|
+
#p raw_output if raw_output
|
149
|
+
if @tail.empty?
|
150
|
+
# The logic of tail: when SET ECHO ON is used, the PRO <end_marker>
|
151
|
+
# statement will be also part of the output. The user will end up
|
152
|
+
# with something like "SQL> pro" as the last line. The solution would
|
153
|
+
# be to search for "pro <end_marker>" and to stop just before "pro", but
|
154
|
+
# the chunk may be split in the middle of the "pro <end_marker>" text and
|
155
|
+
# this is something which depends on the output of the executed
|
156
|
+
# statement (e.g. bytes=4086, output=4084 => chunk="...SQL> p"). The
|
157
|
+
# workaround is to delay sending the last TAIL_LENGTH characters until
|
158
|
+
# the next fetch.
|
159
|
+
raw_output, @tail = raw_output.slice!(0...raw_output.length-TAIL_LENGTH), raw_output
|
160
|
+
else
|
161
|
+
@tail = ''
|
162
|
+
end
|
163
|
+
scanner = StringScanner.new(raw_output)
|
164
|
+
while not scanner.eos?
|
165
|
+
if @look_for == @start_marker
|
166
|
+
if text = scanner.scan_until(/#{@look_for}/)
|
167
|
+
if text !~ /pro #{@look_for}/
|
168
|
+
# Only if it's not part of a PROMPT sqlplus command.
|
169
|
+
# This might happen when the "echo" sqlplus option
|
170
|
+
# is ON and the begin marker is included into the
|
171
|
+
# sql pack file. Because we are using big chunks to
|
172
|
+
# read data it's very unlikely that the echoing of the
|
173
|
+
# prompt command to be split in the middle.
|
174
|
+
@look_for = @end_marker
|
175
|
+
end
|
176
|
+
else
|
177
|
+
scanner.terminate
|
178
|
+
end
|
179
|
+
end
|
180
|
+
if @look_for == @end_marker
|
181
|
+
output = scanner.scan(/[^#{@look_for}]*/)
|
182
|
+
if scanner.scan(/#{@look_for}/)
|
183
|
+
output.gsub!(/\n.*?pro \z/, "")
|
184
|
+
# end of output reached
|
185
|
+
scanner.terminate
|
186
|
+
@busy = false
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
chunk = output.force_encoding('UTF-8')
|
192
|
+
if @current_funnel && !chunk.empty?
|
193
|
+
# nokogiri may be confused about those unclosed <p> tags
|
194
|
+
# sqlplus emits, so it's better to get rid of them and use
|
195
|
+
# <br> instead.
|
196
|
+
@current_funnel.write(br_only(chunk))
|
197
|
+
chunk = @current_funnel.read
|
198
|
+
end
|
199
|
+
return chunk
|
200
|
+
end
|
201
|
+
|
202
|
+
# Cancel the currently executing statement. This is supported on Unix
|
203
|
+
# only. On Windows there's no way to send a CTRL+C signal to Sqlplus
|
204
|
+
# without aborting the process. There's an old enhancement request on
|
205
|
+
# Oracle support:
|
206
|
+
#
|
207
|
+
# Bug 8890996: ENH: CONTROL-C SHOULD NOT EXIT WINDOWS CONSOLE SQLPLUS
|
208
|
+
#
|
209
|
+
# So, as soon as we have some fixes from the Oracle guys I will come
|
210
|
+
# back to this method.
|
211
|
+
def cancel
|
212
|
+
raise PlatformNotSupported if ChildProcess.windows?
|
213
|
+
if busy?
|
214
|
+
@process.send_signal 'INT'
|
215
|
+
mark_cancel
|
216
|
+
# read until the cancel marker
|
217
|
+
raw_output = ""
|
218
|
+
until raw_output =~ /#{@cancel_marker}/
|
219
|
+
begin
|
220
|
+
raw_output = @io_read.read_nonblock(1024)
|
221
|
+
yield if block_given?
|
222
|
+
rescue Errno::EAGAIN
|
223
|
+
sleep 0.1
|
224
|
+
end
|
225
|
+
end
|
226
|
+
@busy = false
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Kill the sqlplus process.
|
231
|
+
def terminate
|
232
|
+
@process.stop
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def br_only(chunk)
|
238
|
+
# be prepared for chunks with <p> tag broken in the middle
|
239
|
+
chunk.gsub(/<p>/, "<br>").gsub(/<p\z/, "<br").gsub(/\Ap>/, "br>")
|
240
|
+
end
|
241
|
+
|
242
|
+
def prepare_funnel(convertor_name)
|
243
|
+
convertor = @registered_convertors[convertor_name]
|
244
|
+
if convertor
|
245
|
+
@current_funnel = Output::HTMLFunnel.new(convertor.new)
|
246
|
+
else
|
247
|
+
@current_funnel = nil
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def capture
|
252
|
+
@process.io.stdin.puts("#pro #{@start_marker}")
|
253
|
+
yield
|
254
|
+
@process.io.stdin.puts("#pro #{@end_marker}")
|
255
|
+
@process.io.stdin.puts(".")
|
256
|
+
end
|
257
|
+
|
258
|
+
def mark_cancel
|
259
|
+
@process.io.stdin.puts
|
260
|
+
@process.io.stdin.puts("pro #{@cancel_marker}")
|
261
|
+
end
|
262
|
+
|
263
|
+
def pack(command, opts)
|
264
|
+
pack_file = opts[:pack_file]
|
265
|
+
if pack_file
|
266
|
+
File.open(pack_file, 'w') do |f|
|
267
|
+
f.puts opts[:prep]
|
268
|
+
f.puts "#pro #@start_marker"
|
269
|
+
f.puts command.strip
|
270
|
+
f.puts "#pro #@end_marker"
|
271
|
+
f.puts "."
|
272
|
+
f.puts opts[:post]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
pack_file
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vorax
|
4
|
+
|
5
|
+
# Implements an IO pipe to link the stdin and stdout handlers of the
|
6
|
+
# sqlplus process to the ends of this pipe. This is required for Windows
|
7
|
+
# processes only. On Unix it's enough to use a builtin IO object.
|
8
|
+
class VoraxIO < IO
|
9
|
+
|
10
|
+
# A proxy for the original read_nonblock method
|
11
|
+
alias :old_read_nonblock :read_nonblock
|
12
|
+
|
13
|
+
# Creates a new IO.
|
14
|
+
def initialize(*args)
|
15
|
+
super(*args)
|
16
|
+
if ChildProcess.windows?
|
17
|
+
require 'Win32API'
|
18
|
+
@hFile = ChildProcess::Windows::Lib.get_osfhandle(fileno)
|
19
|
+
peek_params = [
|
20
|
+
'L', # handle to pipe to copy from
|
21
|
+
'L', # pointer to data buffer
|
22
|
+
'L', # size, in bytes, of data buffer
|
23
|
+
'L', # pointer to number of bytes read
|
24
|
+
'P', # pointer to total number of bytes available
|
25
|
+
'L'] # pointer to unread bytes in this message
|
26
|
+
@peekNamedPipe = Win32API.new("kernel32", "PeekNamedPipe", peek_params, 'I')
|
27
|
+
read_params = [
|
28
|
+
'L', # handle of file to read
|
29
|
+
'P', # pointer to buffer that receives data
|
30
|
+
'L', # number of bytes to read
|
31
|
+
'P', # pointer to number of bytes read
|
32
|
+
'L'] #pointer to structure for data
|
33
|
+
@readFile = Win32API.new("kernel32", "ReadFile", read_params, 'I')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Read in nonblock mode from the pipe.
|
38
|
+
#
|
39
|
+
# @param bytes [int] the number of bytes to be read at once
|
40
|
+
# @see IO.read_nonblock
|
41
|
+
def read_nonblock(bytes)
|
42
|
+
if ChildProcess.windows?
|
43
|
+
read_file(peek)
|
44
|
+
else
|
45
|
+
old_read_nonblock(bytes)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def peek
|
52
|
+
available = [0].pack('I')
|
53
|
+
if @peekNamedPipe.Call(@hFile, 0, 0, 0, available, 0).zero?
|
54
|
+
raise IOError, 'Named pipe unavailable'
|
55
|
+
end
|
56
|
+
available.unpack('I')[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
def read_file(bytes)
|
60
|
+
if bytes > 0
|
61
|
+
number = [0].pack('I')
|
62
|
+
buffer = ' ' * bytes
|
63
|
+
return '' if @readFile.call(@hFile, buffer, bytes, number, 0).zero?
|
64
|
+
buffer[0...number.unpack('I')[0]]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/lib/vorax.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rubygems' unless defined? Gem
|
4
|
+
require 'logger'
|
5
|
+
require 'childprocess'
|
6
|
+
require 'antlr3'
|
7
|
+
|
8
|
+
# The main Vorax namespace. Everything related to VoraX is part
|
9
|
+
# of this module.
|
10
|
+
module Vorax
|
11
|
+
|
12
|
+
# Sets the logger to be used for debug purposes.
|
13
|
+
# @param logger [Logger] the logger object.
|
14
|
+
def self.logger=(logger)
|
15
|
+
@logger = logger
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get the current logger.
|
19
|
+
def self.logger
|
20
|
+
@logger
|
21
|
+
end
|
22
|
+
|
23
|
+
# Log a debug entry.
|
24
|
+
# @param message [String] the message to be logged.
|
25
|
+
def self.debug(message)
|
26
|
+
if @logger
|
27
|
+
@logger.add(Logger::DEBUG, nil, 'rby') { message }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised when another SqlPlus is already executing.
|
32
|
+
class AnotherExecRunning < StandardError; end
|
33
|
+
|
34
|
+
# Raised when the platform is not supported (see cancel)
|
35
|
+
class PlatformNotSupported < StandardError; end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'vorax/version.rb'
|
40
|
+
require 'vorax/vorax_io.rb'
|
41
|
+
require 'vorax/sqlplus.rb'
|
42
|
+
require 'vorax/base_funnel.rb'
|
43
|
+
require 'vorax/output/html_funnel.rb'
|
44
|
+
require 'vorax/output/html_convertor.rb'
|
45
|
+
require 'vorax/output/vertical_convertor.rb'
|
46
|
+
require 'vorax/output/zip_convertor.rb'
|
47
|
+
require 'vorax/output/pagezip_convertor.rb'
|
48
|
+
require 'vorax/output/tablezip_convertor.rb'
|
49
|
+
require 'vorax/parser/parser.rb'
|
50
|
+
require 'vorax/parser/conn_string.rb'
|
51
|
+
require 'vorax/parser/target_ref.rb'
|
52
|
+
require 'vorax/parser/stmt_inspector.rb'
|
53
|
+
require 'vorax/parser/plsql_walker.rb'
|
54
|
+
require 'vorax/parser/plsql_structure.rb'
|
55
|
+
require 'vorax/parser/grammars/statement.rb'
|
56
|
+
require 'vorax/parser/grammars/alias.rb'
|
57
|
+
require 'vorax/parser/grammars/column.rb'
|
58
|
+
require 'vorax/parser/grammars/package_spec.rb'
|
59
|
+
require 'vorax/parser/grammars/plsql_def.rb'
|
60
|
+
require 'vorax/parser/body_split.rb'
|
data/spec/column_spec.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
include Vorax
|
4
|
+
|
5
|
+
describe 'column' do
|
6
|
+
|
7
|
+
it 'should work with multiple columns' do
|
8
|
+
Parser::Column.new.walk('col1, col2, f(a, b, c) x, 3+5, t.*, owner.tab.col y').should eq(["col1", "col2", "x", "t.*", "y"])
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should work with one column' do
|
12
|
+
Parser::Column.new.walk('col1').should eq(["col1"])
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should work with one column with alias' do
|
16
|
+
Parser::Column.new.walk('col1 as x').should eq(["x"])
|
17
|
+
Parser::Column.new.walk('col1 y').should eq(["y"])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should work with referenced columns' do
|
21
|
+
Parser::Column.new.walk('tab.col1').should eq(["tab.col1"])
|
22
|
+
Parser::Column.new.walk('owner.tab.col2').should eq(["owner.tab.col2"])
|
23
|
+
Parser::Column.new.walk('"owner"."tab wow".col2').should eq(['"owner"."tab wow".col2'])
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should work with expressions' do
|
27
|
+
Parser::Column.new.walk('(select 1 from dual) x, 3+2').should eq(["x"])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should ignore functions without alias' do
|
31
|
+
Parser::Column.new.walk('f(1, 2, 3), my_func(col)').should eq([])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should work with nested expressions' do
|
35
|
+
Parser::Column.new.walk('f(1, g(2), x(3, 2, f(10))), my_func(col)').should eq([])
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|