oraora 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/bin/oraora +48 -0
- data/lib/oraora.rb +9 -0
- data/lib/oraora/app.rb +298 -0
- data/lib/oraora/awareness.rb +78 -0
- data/lib/oraora/completion.rb +56 -0
- data/lib/oraora/context.rb +90 -0
- data/lib/oraora/credentials.rb +64 -0
- data/lib/oraora/logger.rb +19 -0
- data/lib/oraora/meta.rb +39 -0
- data/lib/oraora/meta/column.rb +45 -0
- data/lib/oraora/meta/database.rb +27 -0
- data/lib/oraora/meta/materialized_view.rb +48 -0
- data/lib/oraora/meta/object.rb +46 -0
- data/lib/oraora/meta/schema.rb +40 -0
- data/lib/oraora/meta/sequence.rb +33 -0
- data/lib/oraora/meta/subprogram.rb +36 -0
- data/lib/oraora/meta/table.rb +42 -0
- data/lib/oraora/meta/view.rb +41 -0
- data/lib/oraora/oci.rb +56 -0
- data/lib/oraora/terminal.rb +55 -0
- data/oraora.gemspec +22 -0
- data/spec/context_spec.rb +132 -0
- data/spec/credentials_spec.rb +83 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea908b533f803251dad4a3c654c32da376f4026b
|
4
|
+
data.tar.gz: 708a8b06e602feaf36619098b5f585d6bed3505a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9d0bad778bd145d9244ce38bf32bfba433d6c259bca2669e8666657e2818cd953ca5f5ffae409c446b57da319b6cc8892739a4c2c2d3daefbd5fdc0c8dc64fc
|
7
|
+
data.tar.gz: 59cc924133dcafbbf68d78b70606a6fb3a3d63122cec05fed7c6a4c29794ab700f61ac1437a2240a02c92681e8d52db658e0e020a7f1b56751e533a14899b2cf
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Kombajn Zbożowy
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# Oraora
|
2
|
+
|
3
|
+
Oraora is a command-line utility for interacting with Oracle database.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* Command line history
|
8
|
+
* Input TAB-completion
|
9
|
+
* Password file support
|
10
|
+
* Metadata querying
|
11
|
+
* Context-aware SQL
|
12
|
+
* Readable colored output
|
13
|
+
* su/sudo as SYS
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Oraora comes bundled as a Ruby gem. To install just run:
|
18
|
+
```
|
19
|
+
$ gem install oraora
|
20
|
+
```
|
21
|
+
|
22
|
+
If you don't have Ruby, check one-click installer for Windows or rvm for Linux.
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Start oraora passing connection string as argument, just like you would connect to SQL*Plus:
|
27
|
+
```
|
28
|
+
$ oraora user/password@DB
|
29
|
+
```
|
30
|
+
|
31
|
+
OS authentication is supported (pass `/`).
|
32
|
+
|
33
|
+
Roles are supported (append `as SYSDBA` / `as SYSOPER`).
|
34
|
+
|
35
|
+
### Passfile
|
36
|
+
|
37
|
+
Oraora attempts to read file `.orapass` in your home directory if it exists. It should contain connection strings in
|
38
|
+
`user/password@DB` format. Then it's enough to provide `user@DB` when connecting and oraora will automatically fill
|
39
|
+
the password.
|
40
|
+
```
|
41
|
+
$ oraora --log-level=debug user@DB
|
42
|
+
[DEBUG] Connecting: user/password@DB
|
43
|
+
```
|
44
|
+
|
45
|
+
### Context
|
46
|
+
|
47
|
+
Use `c` command to navigate through database like directory structure.
|
48
|
+
```
|
49
|
+
~ $ c some_table # Starting at home schema, navigate into table some_table
|
50
|
+
~.SOME_TABLE $ c col1 # Navigate into column col1
|
51
|
+
~.SOME_TABLE.COL1 $ c - # Navigate up
|
52
|
+
~.SOME_TABLE $ c -- # Navigate two levels up - to database level. You could also use 'cd .'
|
53
|
+
. $ c HR.EMPLOYEES # Navigate to schema HR, table EMPLOYEES
|
54
|
+
HR.EMPLOYEES $ c -/DEPARTMENTS # Navigate up, then to table DEPARTMENTS
|
55
|
+
HR.DEPARTMENTS $ c . # Navigate to root (database level)
|
56
|
+
/ $ c # Navigate to your home schema
|
57
|
+
```
|
58
|
+
|
59
|
+
Note: `c` is aliased as `cd` for unix addicts.
|
60
|
+
|
61
|
+
### Listing and describing objects
|
62
|
+
|
63
|
+
Use `d` command to describe object currently in context.
|
64
|
+
```
|
65
|
+
HR.EMPLOYEES $ d
|
66
|
+
Schema: HR
|
67
|
+
Name: EMPLOYEES
|
68
|
+
Partitioned: NO
|
69
|
+
```
|
70
|
+
|
71
|
+
Use `l` command to list for object currently in context. In database schemas are listed. In schema - objects. In table,
|
72
|
+
view or materialized view - columns, etc.
|
73
|
+
```
|
74
|
+
HR.EMPLOYEES $ l
|
75
|
+
EMPLOYEE_ID PHONE_NUMBER COMMISSION_PCT
|
76
|
+
FIRST_NAME HIRE_DATE MANAGER_ID
|
77
|
+
LAST_NAME JOB_ID DEPARTMENT_ID
|
78
|
+
EMAIL SALARY
|
79
|
+
```
|
80
|
+
|
81
|
+
You can also provide context path as additional parameter for list and describe:
|
82
|
+
```
|
83
|
+
HR.EMPLOYEES $ l .SYS.DUAL
|
84
|
+
DUMMY
|
85
|
+
```
|
86
|
+
|
87
|
+
For list provide filter as last segment of the path:
|
88
|
+
```
|
89
|
+
~ $ l .HR.EMPLOYEES.*NAME
|
90
|
+
FIRST_NAME LAST_NAME
|
91
|
+
```
|
92
|
+
|
93
|
+
Note: `l` is aliased as `ls`. `d` is aliased as `desc` and `describe`.
|
94
|
+
|
95
|
+
### SQL
|
96
|
+
|
97
|
+
Any input starting with SQL keyword like `SELECT`, `INSERT`, `CREATE`, ... is recognized as SQL and passed to database
|
98
|
+
for execution.
|
99
|
+
|
100
|
+
### Context-aware SQL
|
101
|
+
|
102
|
+
When in specific context, you can omit some obvious parts of SQL statements. For example, in context of a table following
|
103
|
+
statements will work:
|
104
|
+
```
|
105
|
+
~.SOME_TABLE $ SELECT; # implicit '... FROM SOME_TABLE'
|
106
|
+
~.SOME_TABLE $ WHERE col = 1; # implicit 'SELECT * FROM SOME_TABLE ...'
|
107
|
+
~.SOME_TABLE $ SET col = 2; # implicit 'UPDATE SOME_TABLE ...'
|
108
|
+
~.SOME_TABLE $ ADD x INTEGER; # implicit 'ALTER TABLE SOME_TABLE ...'
|
109
|
+
```
|
110
|
+
|
111
|
+
Some other examples:
|
112
|
+
```
|
113
|
+
~ $ IDENTIFIED BY oraora; # implicit 'ALTER USER xxx ...'
|
114
|
+
~.SOME_TABLE.COL $ WHERE x = 1; # implicit 'SELECT COL FROM SOME_TABLE ...'
|
115
|
+
~.SOME_TABLE.COL $ RENAME TO kol; # implicit 'ALTER TABLE SOME_TABLE RENAME COLUMN col TO kol'
|
116
|
+
```
|
117
|
+
|
118
|
+
### Su / sudo
|
119
|
+
|
120
|
+
`su` and `sudo` allow to switch to SYS session temporarily or execute a single statement as SYS, similarly to their
|
121
|
+
unix counterparts. If you don't have SYS password for current connection in orafile, you will be prompted for it.
|
122
|
+
```
|
123
|
+
$ oraora foo@DB
|
124
|
+
~ $ SELECT * FROM boo.test;
|
125
|
+
ERROR: Insufficient privileges
|
126
|
+
~ $ sudo GRANT SELECT ON boo.test TO foo;
|
127
|
+
Grant succeeded.
|
128
|
+
~ $ SELECT * FROM boo.test;
|
129
|
+
text
|
130
|
+
------------
|
131
|
+
Hello world!
|
132
|
+
```
|
133
|
+
|
134
|
+
## Limitations
|
135
|
+
|
136
|
+
This is an early alpha version. Things may crash and bugs are hiding out there.
|
137
|
+
|
138
|
+
PL/SQL blocks are not supported (yet).
|
139
|
+
|
140
|
+
Oraora does not implement SQL*Plus-specific commands. `rem`, `set`, `show`, `desc`, `exec`, etc. are not
|
141
|
+
supported.
|
data/bin/oraora
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'oraora'
|
4
|
+
require 'optparse'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
begin
|
8
|
+
# Options
|
9
|
+
options = { log_level: Logger::INFO }
|
10
|
+
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: oraora [options] connection"
|
13
|
+
|
14
|
+
opts.on("-l LEVEL", "--log-level=LEVEL", [:debug, :info, :warn, :error], "Set message verbosity (debug, info, warn, error)") do |l|
|
15
|
+
options[:log_level] = Logger.const_get(l.upcase)
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-h", "--help", "Show this message") { |h| puts opts; exit }
|
19
|
+
end.parse!
|
20
|
+
|
21
|
+
# Logger
|
22
|
+
logger = Oraora::Logger.new(STDOUT, options[:log_level])
|
23
|
+
|
24
|
+
# Read passfile
|
25
|
+
if File.file?(passfile = ENV['HOME'] + '/.orapass')
|
26
|
+
ok = Oraora::Credentials.read_passfile(passfile)
|
27
|
+
logger.warn "There were invalid entries in orapass file, which were ignored" if !ok
|
28
|
+
end
|
29
|
+
|
30
|
+
# Command line arguments
|
31
|
+
credentials = Oraora::Credentials.parse(ARGV[0])
|
32
|
+
credentials.fill_password_from_vault
|
33
|
+
role = ARGV[2] if ARGV[1] == 'as'
|
34
|
+
|
35
|
+
# Run application
|
36
|
+
app = Oraora::App.new(credentials, role, logger)
|
37
|
+
app.run
|
38
|
+
|
39
|
+
rescue OptionParser::ParseError => e
|
40
|
+
puts "Options error: " + e.message
|
41
|
+
|
42
|
+
rescue Oraora::Credentials::ParseError => e
|
43
|
+
puts "Invalid connection string: " + e.message
|
44
|
+
|
45
|
+
rescue OCIError => e
|
46
|
+
logger.error "#{e.message}"
|
47
|
+
|
48
|
+
end
|
data/lib/oraora.rb
ADDED
data/lib/oraora/app.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
module Oraora
|
2
|
+
class App
|
3
|
+
class InvalidCommand < StandardError; end
|
4
|
+
|
5
|
+
SQL_INITIAL_KEYWORDS = %w(
|
6
|
+
SELECT COUNT FROM PARTITION WHERE CONNECT GROUP MODEL UNION INTERSECT MINUS ORDER
|
7
|
+
INSERT UPDATE SET DELETE MERGE
|
8
|
+
TRUNCATE ADD DROP CREATE RENAME ALTER PURGE GRANT REVOKE
|
9
|
+
COMPILE ANALYZE COMMIT ROLLBACK
|
10
|
+
IDENTIFIED PROFILE ACCOUNT QUOTA DEFAULT TEMPORARY
|
11
|
+
)
|
12
|
+
SQL_KEYWORDS = SQL_INITIAL_KEYWORDS + %w(
|
13
|
+
TABLE VIEW MATERIALIZED COLUMN PROCEDURE FUNCTION PACKAGE TYPE BODY
|
14
|
+
USER SESSION SCHEMA SYSTEM DATABASE
|
15
|
+
REPLACE AND OR
|
16
|
+
)
|
17
|
+
ORAORA_KEYWORDS = %w(c cd l ls d desc describe x exit su sudo - -- --- . ! /)
|
18
|
+
|
19
|
+
attr_reader :meta, :context
|
20
|
+
|
21
|
+
def initialize(credentials, role, logger, context = nil)
|
22
|
+
@credentials = credentials
|
23
|
+
@user, @database, @role = (credentials.user ? credentials.user.upcase : nil), credentials.database, (role ? role.upcase.to_sym : nil)
|
24
|
+
@logger = logger
|
25
|
+
@context = context
|
26
|
+
end
|
27
|
+
|
28
|
+
# Run the application with given credentials
|
29
|
+
def run(command = nil)
|
30
|
+
last_interrupt = Time.now - 2
|
31
|
+
|
32
|
+
# Connect to Oracle
|
33
|
+
@logger.debug "Connecting: #{@credentials}" + (@role ? " as #{@role}" : '')
|
34
|
+
logon
|
35
|
+
@user ||= @oci.username
|
36
|
+
@context ||= Context.new(@user, schema: @user)
|
37
|
+
|
38
|
+
# Readline tab completion
|
39
|
+
Readline.completion_append_character = ''
|
40
|
+
Readline.completion_proc = Completion.new(self).comp_proc
|
41
|
+
|
42
|
+
if command
|
43
|
+
process(command)
|
44
|
+
else
|
45
|
+
# Main loop
|
46
|
+
buffer = ''
|
47
|
+
prompt = @context.prompt + ' ' + (@role== :SYSDBA ? '#' : '$') + ' '
|
48
|
+
|
49
|
+
while !@terminate do
|
50
|
+
begin
|
51
|
+
line = Readline.readline(prompt.green.bold)
|
52
|
+
break if !line
|
53
|
+
|
54
|
+
line.strip!
|
55
|
+
Readline::HISTORY << line if line != '' # Manually add to history to avoid empty lines
|
56
|
+
buffer += (buffer == '' ? '' : "\n") + line
|
57
|
+
|
58
|
+
# Process buffer on one of these conditions:
|
59
|
+
# * This is first line of the buffer and is empty
|
60
|
+
# * This is first line of the buffer and is a Oraora command
|
61
|
+
# * Entire buffer is a comment
|
62
|
+
# * Line is '/' or ends with ';'
|
63
|
+
if (buffer == line && (line =~ /^(#{ORAORA_KEYWORDS.collect { |k| Regexp.escape(k) }.join('|')})($|\s+)/i || line =~ /^\s*$/)) || line == '/' || line =~ /;$/ || buffer =~ /\A\s*--/ || buffer =~ /\A\s*\/\*.*\*\/\s*\Z/m
|
64
|
+
process(buffer)
|
65
|
+
buffer = ''
|
66
|
+
end
|
67
|
+
|
68
|
+
if buffer == ''
|
69
|
+
prompt = @context.prompt + ' ' + (@role == :SYSDBA ? '#' : '$') + ' '
|
70
|
+
else
|
71
|
+
prompt = @context.prompt.gsub(/./, ' ') + ' % '
|
72
|
+
end
|
73
|
+
|
74
|
+
rescue Interrupt
|
75
|
+
if Time.now - last_interrupt < 2
|
76
|
+
@logger.warn "Exit on CTRL+C, "
|
77
|
+
terminate
|
78
|
+
else
|
79
|
+
@logger.warn "CTRL+C, hit again within 2 seconds to quit"
|
80
|
+
buffer = ''
|
81
|
+
prompt = @context.prompt + ' ' + (@role == :SYSDBA ? '#' : '$') + ' '
|
82
|
+
last_interrupt = Time.now
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
if !@terminate
|
90
|
+
@logger.debug "Exiting on end of input"
|
91
|
+
terminate
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Logon to the server
|
98
|
+
def logon
|
99
|
+
begin
|
100
|
+
@oci = OCI.new(@user, @credentials.password, @database, @role)
|
101
|
+
@meta = Meta.new(@oci)
|
102
|
+
rescue Interrupt
|
103
|
+
@logger.warn "CTRL+C, aborting logon"
|
104
|
+
exit!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Log off the server and terminate
|
109
|
+
def terminate
|
110
|
+
if @oci
|
111
|
+
@logger.debug "Logging off"
|
112
|
+
@oci.logoff
|
113
|
+
end
|
114
|
+
@terminate = true
|
115
|
+
rescue Interrupt
|
116
|
+
@logger.warn "Interrupt on logoff, force exit"
|
117
|
+
exit!
|
118
|
+
end
|
119
|
+
|
120
|
+
# Parse command options from arguments
|
121
|
+
# Returns options hash and the remaining argument untouched
|
122
|
+
def options_for(args)
|
123
|
+
options = {}
|
124
|
+
while (args =~ /^-[[:alnum:]]/) do
|
125
|
+
opts, args = args.split(/\s+/, 2)
|
126
|
+
@logger.debug "Raw options: #{opts}"
|
127
|
+
opts.gsub(/^-/, '').split('').each do |o|
|
128
|
+
options[o.downcase] = true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
@logger.debug "Options: #{options}"
|
132
|
+
[options, args]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Process the command buffer
|
136
|
+
def process(text)
|
137
|
+
@logger.debug "Processing buffer: #{text}"
|
138
|
+
|
139
|
+
# shortcuts for '.' and '-'
|
140
|
+
text = 'c ' + text if text =~ /^\s*(\.|\-+)\s*$/
|
141
|
+
|
142
|
+
# Determine first non-comment word of a command
|
143
|
+
text =~ /\A(?:\/\*.*?\*\/\s*|--.*?(?:\n|\Z))*\s*([^[:space:]\*\(\/;]+)?\s*(.*?)(?:[[:space:];]*)\Z/mi
|
144
|
+
# <------------- 1 ---------------> <--------- 2 -------->- < 3 >
|
145
|
+
# 1) strip '/* ... */' or '--' style comments from the beginning
|
146
|
+
# 2) first word (any characters not being a space, '(', ';' or '*'), captured into $1
|
147
|
+
# 3) remaining portion of a command, captured into $2
|
148
|
+
|
149
|
+
case first_word = $1 && $1.upcase
|
150
|
+
# Nothing, gibberish or just comments
|
151
|
+
when nil
|
152
|
+
if $2 && $2 != ''
|
153
|
+
raise InvalidCommand, "Invalid command: #{$2}"
|
154
|
+
end
|
155
|
+
|
156
|
+
when 'C', 'CD'
|
157
|
+
@logger.debug "Switch context"
|
158
|
+
old_schema = @context.schema || @context.user
|
159
|
+
if $2 && $2 != ''
|
160
|
+
@context = context_for(@context, $2[/^\S+/])
|
161
|
+
else
|
162
|
+
@context.set(schema: @user)
|
163
|
+
end
|
164
|
+
@logger.debug "New context is #{@context.send(:key_hash)}"
|
165
|
+
if old_schema != (@context.schema || @context.user)
|
166
|
+
@logger.debug "Implicit ALTER SESSION SET CURRENT_SCHEMA = " + (@context.schema || @context.user)
|
167
|
+
@oci.exec("ALTER SESSION SET CURRENT_SCHEMA = " + (@context.schema || @context.user))
|
168
|
+
end
|
169
|
+
|
170
|
+
when 'L', 'LS'
|
171
|
+
@logger.debug "List"
|
172
|
+
options, path = options_for($2)
|
173
|
+
filter = path ? path[/[^\.\/]*(\*|\?)[^\.\/]*$/] : nil
|
174
|
+
path.chomp!(filter) if filter
|
175
|
+
path = path.chomp('.').chomp('/')[/^\S+/] unless path.nil? || path == '.' || path == '/'
|
176
|
+
filter.upcase! if filter
|
177
|
+
@logger.debug "Path: #{path}, Filter: #{filter}"
|
178
|
+
work_context = context_for(@context, path)
|
179
|
+
@logger.debug "List for #{work_context.level || 'database'}"
|
180
|
+
Terminal.puts_grid(@meta.find(work_context).list(options, filter))
|
181
|
+
|
182
|
+
when 'D', 'DESC', 'DESCRIBE'
|
183
|
+
@logger.debug "Describe"
|
184
|
+
options, args = options_for($2)
|
185
|
+
path = args.split(/\s+/).first rescue nil
|
186
|
+
work_context = context_for(@context, path)
|
187
|
+
@logger.debug "Describe for #{work_context.level || 'database'}"
|
188
|
+
puts(@meta.find(work_context).describe(options))
|
189
|
+
|
190
|
+
# TODO: For refactoring
|
191
|
+
if work_context.level == :column && options['p']
|
192
|
+
prof = @oci.exec <<-SQL
|
193
|
+
SELECT value, cnt, rank
|
194
|
+
FROM (SELECT value, cnt, row_number() over (order by cnt desc) AS rank
|
195
|
+
FROM (SELECT #{work_context.column} AS value, count(*) AS cnt
|
196
|
+
FROM #{work_context.object}
|
197
|
+
GROUP BY #{work_context.column}
|
198
|
+
)
|
199
|
+
)
|
200
|
+
WHERE rank <= 20 OR value IS NULL
|
201
|
+
ORDER BY rank
|
202
|
+
SQL
|
203
|
+
puts ""
|
204
|
+
Terminal.puts_cursor(prof)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Exit
|
208
|
+
when 'X', 'EXIT'
|
209
|
+
@logger.debug "Exiting on exit command"
|
210
|
+
terminate
|
211
|
+
|
212
|
+
# SQL
|
213
|
+
when *SQL_INITIAL_KEYWORDS
|
214
|
+
raw_sql = text.gsub(/[;\/]\Z/, '')
|
215
|
+
@logger.debug "SQL: #{raw_sql}"
|
216
|
+
context_aware_sql = Awareness.enrich(raw_sql, @context)
|
217
|
+
@logger.debug "SQL (context-aware): #{context_aware_sql}" if context_aware_sql != raw_sql
|
218
|
+
res = @oci.exec(context_aware_sql)
|
219
|
+
|
220
|
+
if res.is_a? OCI8::Cursor
|
221
|
+
Terminal.puts_cursor(res)
|
222
|
+
@logger.info "#{res.row_count} row(s) selected"
|
223
|
+
else
|
224
|
+
@logger.info "#{res} row(s) affected"
|
225
|
+
end
|
226
|
+
|
227
|
+
when 'SU'
|
228
|
+
@logger.debug "Command type: su"
|
229
|
+
su
|
230
|
+
|
231
|
+
when 'SUDO'
|
232
|
+
@logger.debug "Command type: sudo (#{$2})"
|
233
|
+
raise InvalidCommand, "Command required for sudo" if $2.strip == ''
|
234
|
+
su($2)
|
235
|
+
|
236
|
+
when '!'
|
237
|
+
@logger.debug "Command type: metadata refresh"
|
238
|
+
@meta.purge_cache
|
239
|
+
|
240
|
+
# Unknown
|
241
|
+
else
|
242
|
+
raise InvalidCommand, "Invalid command: #{$1}"
|
243
|
+
end
|
244
|
+
|
245
|
+
rescue InvalidCommand, Meta::NotApplicable => e
|
246
|
+
@logger.error e.message
|
247
|
+
rescue Context::InvalidKey, Meta::NotExists => e
|
248
|
+
@logger.error "Invalid path"
|
249
|
+
rescue OCIError => e
|
250
|
+
@logger.error e.parse_error_offset ? "#{e.message} at #{e.parse_error_offset}" : e.message
|
251
|
+
rescue Interrupt
|
252
|
+
@logger.warn "Interrupted by user"
|
253
|
+
rescue StandardError => e
|
254
|
+
@logger.error "Internal error"
|
255
|
+
@logger.debug e.backtrace
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns new context relative to current one, traversing given path
|
259
|
+
def context_for(context, path)
|
260
|
+
return context.dup if !path || path == ''
|
261
|
+
new_context = context.dup
|
262
|
+
nodes = path.split(/[\.\/]/).collect(&:upcase) rescue []
|
263
|
+
return new_context.root if nodes.empty?
|
264
|
+
level = nodes[0] == '' ? nil : new_context.level
|
265
|
+
|
266
|
+
nodes.each_with_index do |node, i|
|
267
|
+
case
|
268
|
+
when i.zero? && node == '' then new_context.root
|
269
|
+
when i.zero? && node == '~' then new_context.set(schema: @user)
|
270
|
+
when node == '-' then new_context.up
|
271
|
+
when node == '--' then new_context.up.up
|
272
|
+
when node =~ /^-+$/ then new_context.up.up.up
|
273
|
+
else
|
274
|
+
raise Context::InvalidKey if node !~ /^[a-zA-Z0-9_\$]{,30}$/
|
275
|
+
case new_context.level
|
276
|
+
when nil
|
277
|
+
@meta.find(new_context.traverse(schema: node))
|
278
|
+
when :schema
|
279
|
+
o = @meta.find_object(new_context.schema, node)
|
280
|
+
new_context.traverse(object: node, object_type: o.type)
|
281
|
+
when :object
|
282
|
+
@meta.find(new_context.traverse(column: node))
|
283
|
+
#TODO: Subprograms
|
284
|
+
else raise Context::InvalidKey
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
new_context
|
289
|
+
end
|
290
|
+
|
291
|
+
# Gets SYS password either from orapass file or user input, then spawns subshell
|
292
|
+
def su(command = nil)
|
293
|
+
su_credentials = Credentials.new('sys', nil, @database).fill_password_from_vault
|
294
|
+
su_credentials.password = ask("SYS password: ") { |q| q.echo = '' } if !su_credentials.password
|
295
|
+
App.new(su_credentials, :SYSDBA, @logger, @context.su('SYS')).run(command)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|