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.
@@ -0,0 +1,78 @@
1
+ module Oraora
2
+ module Awareness
3
+ Entry = Struct.new(:key, :object_types, :map, :sql)
4
+
5
+ ENRICH_MAP = [
6
+ # Column
7
+ Entry.new(:column, nil, %w(SELECT ;), 'SELECT $column FROM $object'),
8
+ *%w(PARTITION WHERE CONNECT GROUP MODEL UNION INTERSECT MINUS ORDER).collect do |keyword|
9
+ Entry.new(:column, nil, [keyword], "SELECT $column FROM $object #{keyword}")
10
+ end,
11
+ Entry.new(:column, nil, %w(SET), 'UPDATE $object SET $column ='),
12
+ Entry.new(:column, 'TABLE', %w(DROP), 'ALTER TABLE $object DROP COLUMN $column'),
13
+ Entry.new(:column, 'TABLE', %w(RENAME), 'ALTER TABLE $table RENAME COLUMN $column TO'),
14
+
15
+ # Table
16
+ Entry.new(:object, 'TABLE', %w(DROP ;), 'DROP TABLE $object'),
17
+ Entry.new(:object, 'TABLE', %w(DROP CASCADE), 'DROP TABLE $object CASCADE'),
18
+ Entry.new(:object, 'TABLE', %w(DROP PURGE), 'DROP TABLE $object PURGE'),
19
+ Entry.new(:object, 'TABLE', %w(TRUNCATE), 'TRUNCATE TABLE $object'),
20
+ Entry.new(:object, 'TABLE', %w(DROP PARTITION), 'ALTER TABLE $object DROP PARTITION'),
21
+ *%w(ADD MODIFY RENAME PARALLEL NOPARALLEL ENABLE DISABLE CACHE NOCACHE READ REKEY PCTFREE PCTUSED INITRANS COMPRESS NOCOMPRESS SHRINK MERGE SPLIT).collect do |keyword|
22
+ Entry.new(:object, 'TABLE', [keyword], "ALTER TABLE $object #{keyword}")
23
+ end,
24
+
25
+ # View
26
+ Entry.new(:object, 'VIEW', %w(DROP), 'DROP VIEW $object'),
27
+ Entry.new(:object, 'VIEW', %w(RENAME), 'RENAME $object'),
28
+ Entry.new(:object, 'VIEW', %w(COMPILE), 'ALTER VIEW $object COMPILE'),
29
+
30
+ # Mview
31
+ Entry.new(:object, 'MATERIALIZED VIEW', %w(DROP), 'DROP MATERIALIZED VIEW $object'),
32
+
33
+ # Relation
34
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(SELECT ;), 'SELECT * FROM $object'),
35
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(COUNT ), 'SELECT count(*) FROM $object'),
36
+ *%w(PARTITION WHERE CONNECT GROUP MODEL UNION INTERSECT MINUS ORDER).collect do |keyword|
37
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], [keyword], "SELECT * FROM $object #{keyword}")
38
+ end,
39
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(SET), 'UPDATE $object SET'),
40
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(DELETE ;), 'DELETE FROM $object'),
41
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(DELETE WHERE), 'DELETE FROM $object WHERE'),
42
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(INSERT VALUES), 'INSERT INTO $object VALUES'),
43
+ Entry.new(:object, ['TABLE', 'VIEW', 'MATERIALIZED VIEW'], %w(INSERT SELECT), 'INSERT INTO $object SELECT'),
44
+
45
+ # Schema
46
+ *%w(IDENTIFIED PROFILE ACCOUNT QUOTA DEFAULT TEMPORARY).collect do |keyword|
47
+ Entry.new(:schema, nil, [keyword], "ALTER USER $schema #{keyword}")
48
+ end
49
+ ]
50
+
51
+ def self.enrich(sql, context)
52
+ tokens = []
53
+ map = []
54
+ (sql + ';').scan(/(?:\w+|\/\*.*?\*\/|--.*?\n|;|\s+)/mi) do |token|
55
+ tokens << token
56
+ if token =~ /^(\/\*.*?\*\/|--.*?\n|\s+)$/mi
57
+ next
58
+ end
59
+ map << token.upcase
60
+ #puts "[AWARENESS] #{token}, map: #{map}"
61
+ match = ENRICH_MAP.detect do |entry|
62
+ context.send(entry.key) && (!entry.object_types || [*entry.object_types].include?(context.object_type)) && map == entry.map
63
+ end
64
+
65
+ if match
66
+ #puts "[AWARENESS] Map match: #{match.sql}"
67
+ sql =~ /^#{tokens.join.chomp(';')}(.*)$/mi
68
+ last_part = $1
69
+ first_part = eval '"' + match.sql.gsub(/\$(\w+)+/, '#{context.send(:\1)}') + '"'
70
+ return first_part + last_part
71
+ end
72
+
73
+ break if tokens.length >= 2
74
+ end
75
+ sql
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,56 @@
1
+ module Oraora
2
+ class Completion
3
+ TEMPLATES = {
4
+ 's' => 'SELECT ',
5
+ 's*' => 'SELECT * FROM ',
6
+ 'c*' => 'SELECT COUNT(*) FROM ',
7
+ 'i' => 'INSERT ',
8
+ 'u' => 'UPDATE ',
9
+ 'd' => 'DELETE ',
10
+ 'a' => 'ALTER ',
11
+ 'c' => 'CREATE ',
12
+ 'cr' => 'CREATE OR REPLACE '
13
+ }
14
+
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def comp_proc
20
+ Proc.new do |s|
21
+ # Complete with template alone if matched
22
+ if TEMPLATES[Readline.line_buffer.downcase]
23
+ TEMPLATES[Readline.line_buffer.downcase]
24
+
25
+ else
26
+ # Complete for SQL keywords
27
+ comp = App::SQL_KEYWORDS
28
+
29
+ # Complete for current context
30
+ if s !~ /[\.\/]/
31
+ comp += @app.meta.find(@app.context).list rescue []
32
+ context = @app.context.dup
33
+ comp += @app.meta.find(context.up).list while context.level
34
+
35
+ # Complete for input
36
+ else
37
+ context = @app.context.dup
38
+ path = s.split(/(?<=[\.\/])/, -1)
39
+ last = path.pop
40
+ loop do
41
+ comp_context = @app.context_for(context, path.join) rescue nil
42
+ if comp_context
43
+ comp += @app.meta.find(comp_context).list.collect { |n| path.join + n } rescue []
44
+ end
45
+ break if context.level == nil
46
+ context.up
47
+ end
48
+ end
49
+
50
+ comp.sort.uniq.grep(/^#{Regexp.escape(s)}/i)
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,90 @@
1
+ module Oraora
2
+ class Context
3
+ class InvalidKey < StandardError; end
4
+
5
+ HIERARCHY = {
6
+ nil => [:schema],
7
+ schema: [:object],
8
+ object: [:column, :subprogram],
9
+ column: [],
10
+ subprogram: [],
11
+ }
12
+ KEYS = HIERARCHY.keys.compact + [:object_type, :subprogram_type]
13
+ RELATION_OBJECT_TYPES = ['TABLE', 'VIEW', 'MATERIALIZED VIEW']
14
+
15
+ attr_reader :level, :user, *KEYS
16
+
17
+ def initialize(user = nil, hash = {})
18
+ @user = user
19
+ set(hash)
20
+ end
21
+
22
+ def su(user)
23
+ self.class.new(user, key_hash)
24
+ end
25
+
26
+ def dup
27
+ su(@user)
28
+ end
29
+
30
+ def set(hash = {})
31
+ KEYS.each { |key| instance_variable_set("@#{key}", nil) }
32
+ @level = nil
33
+ traverse(hash)
34
+ end
35
+
36
+ def traverse(hash)
37
+ while(!hash.empty?) do
38
+ key = HIERARCHY[@level].detect { |k| hash[k] } or raise InvalidKey
39
+ case key
40
+ when :column then raise InvalidKey unless RELATION_OBJECT_TYPES.include?(@object_type)
41
+ when :object then raise InvalidKey unless @object_type = hash.delete(:object_type)
42
+ when :subprogram then raise InvalidKey unless @object_type == :package && @subprogram_type = hash.delete(:subprogram_type)
43
+ end
44
+ @level = key
45
+ instance_variable_set("@#{key}", hash.delete(key))
46
+ end
47
+ self
48
+ end
49
+
50
+ def root
51
+ set
52
+ end
53
+
54
+ def up
55
+ case @level
56
+ when nil then return self
57
+ when :subprogram then @subprogram_type = nil
58
+ when :object then @object_type = nil
59
+ end
60
+ instance_variable_set("@#{level}", nil)
61
+ @level = HIERARCHY.invert.detect { |k, v| k.include? @level }.last
62
+ self
63
+ end
64
+
65
+ def prompt
66
+ if @schema
67
+ p = @user == @schema ? '~' : @schema
68
+ level_2 = @object
69
+ p += ".#{level_2}" if level_2
70
+ level_3 = @column || @subprogram
71
+ p += ".#{level_3}" if level_3
72
+ else
73
+ p = '/'
74
+ end
75
+ p
76
+ end
77
+
78
+ def key_hash
79
+ Hash[ KEYS.collect { |key| [key, instance_variable_get("@#{key}")] } ].delete_if { |k, v| v.nil? }
80
+ end
81
+
82
+ def hash
83
+ key_hash.hash
84
+ end
85
+
86
+ def eql?(other)
87
+ key_hash == other.key_hash
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,64 @@
1
+ module Oraora
2
+ class Credentials
3
+ class ParseError < StandardError; end
4
+
5
+ attr_accessor :user, :password, :database
6
+ @@vault = []
7
+
8
+ def initialize(user = nil, password = nil, database = nil)
9
+ @user = user
10
+ @password = password
11
+ @database = database
12
+ end
13
+
14
+ def self.read_passfile(filename)
15
+ @@vault = []
16
+ ok = true
17
+ File.open(filename, "r") do |infile|
18
+ while (line = infile.gets)
19
+ begin
20
+ @@vault << parse(line.chomp)
21
+ rescue ParseError
22
+ ok = false
23
+ end
24
+ end
25
+ end
26
+ ok
27
+ end
28
+
29
+ def self.parse(str)
30
+ if str
31
+ match = str.match /^([^\/@]+)?\/?([^\/@]+)?@?([^\/@]+)?$/
32
+ raise ParseError, "invalid format (use login/pass@DB)" if !match
33
+ user, password, database = match[1..3]
34
+ raise ParseError, "user can only contain alphanumeric characters" if user && !user.match(/^\w+$/)
35
+ raise ParseError, "database name can only contain alphanumeric characters" if database && !database.match(/^\w+$/)
36
+ return new(user, password, database)
37
+ else
38
+ return new
39
+ end
40
+ end
41
+
42
+ def fill_password_from_vault
43
+ entry = @@vault.detect { |e| match?(e) }
44
+ @password = entry.password if entry && !@password
45
+ self
46
+ end
47
+
48
+ def to_s
49
+ s = @user || ''
50
+ s += '/' + @password if @password
51
+ s = '/' if s == ''
52
+ s += '@' + @database if @database
53
+ s
54
+ end
55
+
56
+ def eql?(c)
57
+ user == c.user && password == c.password && database == c.database
58
+ end
59
+
60
+ def match?(c)
61
+ user == c.user && database == c.database
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+ require 'colorize'
3
+
4
+ module Oraora
5
+ class Logger < ::Logger
6
+ SEVERITY_COLORS = {
7
+ 'WARN' => :yellow,
8
+ 'ERROR' => :red,
9
+ 'INFO' => :light_black,
10
+ 'DEBUG' => :light_black
11
+ }
12
+
13
+ def initialize(name, log_level = ::Logger::WARN)
14
+ super
15
+ self.level = log_level
16
+ self.formatter = proc { |severity, datetime, progname, msg| "[#{severity}] #{msg}\n".send(SEVERITY_COLORS[severity]) }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ module Oraora
2
+ # Helper class wrapping OCI methods for querying metadata
3
+ class Meta
4
+ class NotExists < StandardError; end
5
+ class NotApplicable < StandardError; end
6
+
7
+ # Initializes with OCI
8
+ def initialize(oci)
9
+ @oci = oci
10
+ @cache = {}
11
+ end
12
+
13
+ # Returns a node identified by context
14
+ def find(context)
15
+ node = case context.level
16
+ when nil
17
+ @cache[context] || Meta::Database.from_oci(@oci)
18
+ when :schema
19
+ @cache[context] || Meta::Schema.from_oci(@oci, context.schema)
20
+ when :object
21
+ @cache[context] || Meta::Object.from_oci(@oci, context.schema, context.object, context.object_type)
22
+ when :column
23
+ find(context.dup.up).columns(context.column)
24
+ end
25
+ @cache[context] = node if node && context.level != :column
26
+ node
27
+ end
28
+
29
+ # Returns an object node identified by name
30
+ def find_object(schema, name)
31
+ Meta::Object.from_oci(@oci, schema, name)
32
+ end
33
+
34
+ # Removes all cached metadata
35
+ def purge_cache
36
+ @cache = {}
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ module Oraora
2
+ class Meta
3
+ class Column
4
+ CHAR_USED_MAP = { 'B' => 'BYTE', 'C' => 'CHAR' }
5
+ attr_reader :name, :id
6
+
7
+ def initialize(schema, relation, name, attributes = {})
8
+ @schema = schema
9
+ @relation = relation
10
+ @name = name
11
+ attributes.each { |k, v| instance_variable_set("@#{k}".to_sym, v) }
12
+ end
13
+
14
+ def describe(options = {})
15
+ <<-HERE.reset_indentation
16
+ Column #{@schema}.#{@relation}.#{@name}
17
+ Id: #{@id}
18
+ Type: #{display_type}
19
+ HERE
20
+ end
21
+
22
+ def list(options = {}, filter = nil)
23
+ raise NotApplicable, "Nothing to list for column"
24
+ end
25
+
26
+ def display_type
27
+ case @type
28
+ when 'NUMBER'
29
+ case
30
+ when !@precision && !@scale then "NUMBER"
31
+ when !@precision && @scale == 0 then "INTEGER"
32
+ when @scale == 0 then "NUMBER(#{@precision})"
33
+ else "NUMBER(#{@precision},#{@scale})"
34
+ end
35
+ when 'CHAR', 'NCHAR'
36
+ @char_length == 1 ? 'CHAR' : "CHAR(#{@char_length} #{@char_used})"
37
+ when 'VARCHAR', 'VARCHAR2', 'NVARCHAR2'
38
+ "#{@type}(#{@char_length} #{CHAR_USED_MAP[@char_used]})"
39
+ else
40
+ @type
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module Oraora
2
+ class Meta
3
+ class Database
4
+ def load_from_oci(oci)
5
+ @name, @created = oci.select_one("SELECT name, created FROM v$database")
6
+ @schemas = oci.pluck_one("SELECT username FROM all_users ORDER BY username")
7
+ self
8
+ end
9
+
10
+ def self.from_oci(oci)
11
+ new.load_from_oci(oci)
12
+ end
13
+
14
+ def describe(options = {})
15
+ <<-HERE.reset_indentation
16
+ Database #{@name}
17
+ Created: #{@created}
18
+ HERE
19
+ end
20
+
21
+ def list(options = {}, filter = nil)
22
+ schemas = @schemas.select! { |o| o =~ /^#{Regexp.escape(filter).gsub('\*', '.*').gsub('\?', '.')}$/ } if filter
23
+ schemas || @schemas
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ require_relative './object.rb'
2
+
3
+ module Oraora
4
+ class Meta
5
+ class MaterializedView < Object
6
+ def type
7
+ 'MATERIALIZED VIEW'
8
+ end
9
+
10
+ def load_from_oci(oci)
11
+ @updatable, @refresh_mode, @fast_refreshable, @staleness =
12
+ oci.select_one("SELECT updatable, refresh_mode, fast_refreshable, staleness
13
+ FROM all_mviews
14
+ WHERE owner = :schema AND mview_name = :name", @schema, @name)
15
+ raise NotExists if !@updatable
16
+ @columns = oci.pluck("SELECT column_name, column_id, data_type, data_length, data_precision, data_scale, char_used, char_length " +
17
+ "FROM all_tab_columns WHERE owner = :schema AND table_name = :name ORDER BY column_id", @schema, @name).collect do |col|
18
+ Column.new(@schema, @name, col[0], id: col[1].to_i, type: col[2], length: col[3] && col[3].to_i,
19
+ precision: col[4] && col[4].to_i, scale: col[5] && col[5].to_i, char_used: col[6],
20
+ char_length: col[7] && col[7].to_i)
21
+ end
22
+ @columns_hash = Hash[@columns.collect { |col| [col.name, col] }]
23
+ self
24
+ end
25
+
26
+ def describe(options = {})
27
+ <<-HERE.reset_indentation
28
+ Materialized view #{@schema}.#{@name}
29
+ Updatable: #{@updatable}
30
+ Refresh mode: #{@refresh_mode}
31
+ Fast refreshable: #{@fast_refreshable}
32
+ Staleness: #{@staleness}
33
+ HERE
34
+ end
35
+
36
+ def list(options = {}, filter = nil)
37
+ columns = @columns_hash.keys
38
+ columns.select! { |c| c =~ /^#{Regexp.escape(filter).gsub('\*', '.*').gsub('\?', '.')}$/ } if filter
39
+ columns
40
+ end
41
+
42
+ def columns(column)
43
+ raise NotExists if !@columns_hash[column]
44
+ @columns_hash[column]
45
+ end
46
+ end
47
+ end
48
+ end