class Oraora::App
Constants
- ORAORA_KEYWORDS
- SQL_INITIAL_KEYWORDS
- SQL_KEYWORDS
Attributes
context[R]
meta[R]
Public Class Methods
new(credentials, role, logger, context = nil)
click to toggle source
# File lib/oraora/app.rb, line 22 def initialize(credentials, role, logger, context = nil) @credentials = credentials @user, @database, @role = (credentials.user ? credentials.user.upcase : nil), credentials.database, (role ? role.upcase.to_sym : nil) @logger = logger @context = context end
Public Instance Methods
run(command = nil)
click to toggle source
Run the application with given credentials
# File lib/oraora/app.rb, line 30 def run(command = nil) last_interrupt = Time.now - 2 # Connect to Oracle @logger.debug "Connecting: #{@credentials}" + (@role ? " as #{@role}" : '') logon @logger.info "Oraora v#{VERSION}. Type '?' for quick help" @user ||= @oci.username @context ||= Context.new(@user, schema: @user) # Readline tab completion Readline.completion_append_character = '' Readline.completion_proc = Completion.new(self).comp_proc if command process(command) else # Main loop buffer = '' prompt = @context.prompt + ' ' + (@role== :SYSDBA ? '#' : '$') + ' ' while !@terminate do begin line = Readline.readline(prompt.green.bold) break if !line line.strip! Readline::HISTORY << line if line != '' # Manually add to history to avoid empty lines buffer += (buffer == '' ? '' : "\n") + line # Process buffer on one of these conditions: # * This is first line of the buffer and is empty # * This is first line of the buffer and is a Oraora command # * Entire buffer is a comment # * Line is '/' or ends with ';' 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 process(buffer) buffer = '' end if buffer == '' prompt = @context.prompt + ' ' + (@role == :SYSDBA ? '#' : '$') + ' ' else prompt = @context.prompt.gsub(/./, ' ') + ' % ' end rescue Interrupt if Time.now - last_interrupt < 2 @logger.warn "Exit on CTRL+C" terminate else @logger.warn "CTRL+C, hit again within 2 seconds to quit" buffer = '' prompt = @context.prompt + ' ' + (@role == :SYSDBA ? '#' : '$') + ' ' last_interrupt = Time.now end end end end if !@terminate @logger.debug "Exiting on end of input" terminate end end
Private Instance Methods
context_for(context, path)
click to toggle source
Returns new context relative to current one, traversing given path
# File lib/oraora/app.rb, line 265 def context_for(context, path) return context.dup if !path || path == '' new_context = context.dup nodes = path.split(/[\.\/]/).collect(&:upcase) rescue [] return new_context.root if nodes.empty? level = nodes[0] == '' ? nil : new_context.level nodes.each_with_index do |node, i| case when i.zero? && node == '' then new_context.root when i.zero? && node == '~' then new_context.set(schema: @user) when node == '-' then new_context.up when node == '--' then new_context.up.up when node =~ /^-+$/ then new_context.up.up.up else raise Context::InvalidKey if node !~ /^[a-zA-Z0-9_\$]{,30}$/ case new_context.level when nil @meta.find(new_context.traverse(schema: node)) when :schema o = @meta.find_object(new_context.schema, node) new_context.traverse(object: node, object_type: o.type) when :object @meta.find(new_context.traverse(column: node)) #TODO: Subprograms else raise Context::InvalidKey end end end new_context end
logon()
click to toggle source
Logon to the server
# File lib/oraora/app.rb, line 100 def logon begin @oci = OCI.new(@user, @credentials.password, @database, @role) @meta = Meta.new(@oci) rescue Interrupt @logger.warn "CTRL+C, aborting logon" exit! end end
options_for(args)
click to toggle source
Parse command options from arguments Returns options hash and the remaining argument untouched
# File lib/oraora/app.rb, line 124 def options_for(args) options = {} while (args =~ /^-[[:alnum:]]/) do opts, args = args.split(/\s+/, 2) @logger.debug "Raw options: #{opts}" opts.gsub(/^-/, '').split('').each do |o| options[o.downcase] = true end end @logger.debug "Options: #{options}" [options, args] end
process(text)
click to toggle source
Process the command buffer
# File lib/oraora/app.rb, line 138 def process(text) @logger.debug "Processing buffer: #{text}" # shortcuts for '.' and '-' text = 'c ' + text if text =~ /^\s*(\.|\-+)\s*$/ # Determine first non-comment word of a command text =~ /\A(?:\/\*.*?\*\/\s*|--.*?(?:\n|\Z))*\s*([^[:space:]\*\(\/;]+)?\s*(.*?)(?:[[:space:];]*)\Z/mi # <------------- 1 ---------------> <--------- 2 -------->- < 3 > # 1) strip '/* ... */' or '--' style comments from the beginning # 2) first word (any characters not being a space, '(', ';' or '*'), captured into $1 # 3) remaining portion of a command, captured into $2 case first_word = $1 && $1.upcase # Nothing, gibberish or just comments when nil if $2 && $2 != '' raise InvalidCommand, "Invalid command: #{$2}" end when 'C', 'CD' @logger.debug "Switch context" old_schema = @context.schema || @context.user if $2 && $2 != '' @context = context_for(@context, $2[/^\S+/]) else @context.set(schema: @user) end @logger.debug "New context is #{@context.send(:key_hash)}" if old_schema != (@context.schema || @context.user) @logger.debug "Implicit ALTER SESSION SET CURRENT_SCHEMA = " + (@context.schema || @context.user) @oci.exec("ALTER SESSION SET CURRENT_SCHEMA = " + (@context.schema || @context.user)) end when 'L', 'LS' @logger.debug "List" options, path = options_for($2) filter = path ? path[/[^\.\/]*(\*|\?)[^\.\/]*$/] : nil path.chomp!(filter) if filter path = path.chomp('.').chomp('/')[/^\S+/] unless path.nil? || path == '.' || path == '/' filter.upcase! if filter @logger.debug "Path: #{path}, Filter: #{filter}" work_context = context_for(@context, path) @logger.debug "List for #{work_context.level || 'database'}" Terminal.puts_grid(@meta.find(work_context).list(options, filter)) when 'D', 'DESC', 'DESCRIBE' @logger.debug "Describe" options, args = options_for($2) path = args.split(/\s+/).first rescue nil work_context = context_for(@context, path) @logger.debug "Describe for #{work_context.level || 'database'}" puts(@meta.find(work_context).describe(options)) # TODO: For refactoring if work_context.level == :column && options['p'] prof = @oci.exec <<-SQL SELECT value, cnt, rank FROM (SELECT value, cnt, row_number() over (order by cnt desc) AS rank FROM (SELECT #{work_context.column} AS value, count(*) AS cnt FROM #{work_context.object} GROUP BY #{work_context.column} ) ) WHERE rank <= 20 OR value IS NULL ORDER BY rank SQL puts "" Terminal.puts_cursor(prof) end # Exit when 'X', 'EXIT' @logger.debug "Exiting on exit command" terminate # SQL when *SQL_INITIAL_KEYWORDS raw_sql = text.gsub(/[;\/]\Z/, '') @logger.debug "SQL: #{raw_sql}" context_aware_sql = Awareness.enrich(raw_sql, @context) @logger.debug "SQL (context-aware): #{context_aware_sql}" if context_aware_sql != raw_sql res = @oci.exec(context_aware_sql) if res.is_a? OCI8::Cursor Terminal.puts_cursor(res) @logger.info "#{res.row_count} row(s) selected" else @logger.info "#{res} row(s) affected" end when 'SU' @logger.debug "Command type: su" su when 'SUDO' @logger.debug "Command type: sudo (#{$2})" raise InvalidCommand, "Command required for sudo" if $2.strip == '' su($2) when '!' @logger.debug "Command type: metadata refresh" @meta.purge_cache when '?' @logger.debug "Command type: help" puts(HELP) # Unknown else raise InvalidCommand, "Invalid command: #{$1}" end rescue InvalidCommand, Meta::NotApplicable => e @logger.error e.message rescue Context::InvalidKey, Meta::NotExists => e @logger.error "Invalid path" rescue OCIError => e @logger.error e.parse_error_offset ? "#{e.message} at #{e.parse_error_offset}" : e.message rescue Interrupt @logger.warn "Interrupted by user" rescue StandardError => e @logger.error "Internal error" @logger.debug e.backtrace end
su(command = nil)
click to toggle source
Gets SYS password either from orapass file or user input, then spawns subshell
# File lib/oraora/app.rb, line 298 def su(command = nil) su_credentials = Credentials.new('sys', nil, @database).fill_password_from_vault su_credentials.password = ask("SYS password: ") { |q| q.echo = '' } if !su_credentials.password App.new(su_credentials, :SYSDBA, @logger, @context.su('SYS')).run(command) end
terminate()
click to toggle source
Log off the server and terminate
# File lib/oraora/app.rb, line 111 def terminate if @oci @logger.debug "Logging off" @oci.logoff end @terminate = true rescue Interrupt @logger.warn "Interrupt on logoff, force exit" exit! end