oraora 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .ruby-version
4
+ .ruby-gemset
5
+ .rvmrc
6
+
7
+ # RubyMine project
8
+ /.idea
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
@@ -0,0 +1,9 @@
1
+ require 'readline'
2
+ require 'highline'
3
+ require 'highline/import'
4
+ require 'indentation'
5
+ require 'bigdecimal'
6
+ require 'colorize'
7
+ require 'oci8'
8
+
9
+ Dir[File.dirname(__FILE__) + '/oraora/**/*.rb'].each { |file| require file }
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