oraora 0.1.0
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.
- 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
|