vorax 0.1.0pre
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.
- 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
|
+
|