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,46 @@
1
+ module Oraora
2
+ class Meta
3
+ class Object
4
+ attr_reader :type
5
+
6
+ def initialize(schema, name, type = nil)
7
+ @schema = schema
8
+ @name = name
9
+ @type = type
10
+ end
11
+
12
+ def load_from_oci(oci)
13
+ if !@type
14
+ @id, @type = oci.select_one("SELECT object_id, object_type FROM all_objects
15
+ WHERE owner = :schema AND object_name = :name
16
+ ORDER BY decode(namespace, 19, 0, 99)", @schema, @name) if !@type
17
+ raise NotExists if !@id
18
+ @id = @id.to_i
19
+ end
20
+ case @type
21
+ when 'TABLE' then Table.from_oci(oci, @schema, @name)
22
+ when 'VIEW' then View.from_oci(oci, @schema, @name)
23
+ when 'MATERIALIZED VIEW' then MaterializedView.from_oci(oci, @schema, @name)
24
+ when 'SEQUENCE' then Sequence.from_oci(oci, @schema, @name)
25
+ else self
26
+ end
27
+ end
28
+
29
+ def self.from_oci(oci, schema, name, type = nil)
30
+ new(schema, name, type).load_from_oci(oci)
31
+ end
32
+
33
+ def describe(options = {})
34
+ <<-HERE.reset_indentation
35
+ Object #{@schema}.#{@name}
36
+ Id: #{@id}
37
+ Type: #{@type}
38
+ HERE
39
+ end
40
+
41
+ def list(options = {}, filter = nil)
42
+ raise NotApplicable, "Cannot list for this object"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ module Oraora
2
+ class Meta
3
+ class Schema
4
+ def initialize(name)
5
+ @name = name
6
+ end
7
+
8
+ def load_from_oci(oci)
9
+ @id, @created = oci.select_one("SELECT user_id, created FROM all_users WHERE username = :name", @name)
10
+ raise NotExists if !@id
11
+ @id = @id.to_i
12
+ @objects = oci.pluck("SELECT object_name, min(object_type) object_type FROM all_objects
13
+ WHERE owner = :name
14
+ AND object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW', 'SEQUENCE')
15
+ GROUP BY object_name
16
+ ORDER BY object_name", @name)
17
+ self
18
+ end
19
+
20
+ def self.from_oci(oci, name)
21
+ new(name).load_from_oci(oci)
22
+ end
23
+
24
+ def describe(options = {})
25
+ <<-HERE.reset_indentation
26
+ Schema #{@name}
27
+ Id: #{@id}
28
+ Created: #{@created}
29
+ HERE
30
+ end
31
+
32
+ def list(options = {}, filter = nil)
33
+ objects = @objects.collect(&:first)
34
+ objects.reject! { |o| o =~ /^ISEQ\$\$/ || o =~ /^SYS_/ || o =~ /^ORA_/ } unless options['a']
35
+ objects.select! { |o| o =~ /^#{Regexp.escape(filter).gsub('\*', '.*').gsub('\?', '.')}$/ } if filter
36
+ objects
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ require_relative './object.rb'
2
+
3
+ module Oraora
4
+ class Meta
5
+ class Sequence < Object
6
+ def type
7
+ 'SEQUENCE'
8
+ end
9
+
10
+ def load_from_oci(oci)
11
+ @min, @max, @inc, @last = oci.select_one("SELECT min_value, max_value, increment_by, last_number
12
+ FROM all_sequences
13
+ WHERE sequence_owner = :schema AND sequence_name = :name", @schema, @name)
14
+ raise NotExists if !@min
15
+ self
16
+ end
17
+
18
+ def describe(options = {})
19
+ <<-HERE.reset_indentation
20
+ Sequence #{@schema}.#{@name}
21
+ Min value: #{@min.to_i}
22
+ Max value: #{@max.to_i}
23
+ Increment by: #{@inc.to_i}
24
+ Last value: #{@last.to_i}
25
+ HERE
26
+ end
27
+
28
+ def list(options = {}, filter = nil)
29
+ raise NotApplicable, "Nothing to list for sequence"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module Oraora
2
+ class Meta
3
+ class Subprogram
4
+ attr_reader :id, :schema, :package, :name
5
+
6
+ def initialize(schema, package, name)
7
+ @schema = schema
8
+ @package = package
9
+ @name = name
10
+ end
11
+
12
+ def load_from_oci(oci)
13
+ @id =
14
+ oci.select_one("SELECT subprogram_id FROM all_procedues WHERE owner = :schema AND object_name = :package AND procedure_name = :name", @schema, @package, @name)
15
+ self
16
+ end
17
+
18
+ def self.from_oci(oci, schema, package, name)
19
+ new(schema, package, name).load_from_oci(oci)
20
+ end
21
+
22
+ def describe(options = {})
23
+ <<-HERE.reset_indentation
24
+ Schema: #{@schema}
25
+ Package: #{@package}
26
+ Name: #{@name}
27
+ Id: #{@id}
28
+ HERE
29
+ end
30
+
31
+ def list(options = {}, filter = nil)
32
+ raise NotApplicable, "Nothing to list for subprogram"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ require_relative './object.rb'
2
+
3
+ module Oraora
4
+ class Meta
5
+ class Table < Object
6
+ def type
7
+ 'TABLE'
8
+ end
9
+
10
+ def load_from_oci(oci)
11
+ @partitioned = oci.select_one("SELECT partitioned FROM all_tables WHERE owner = :schema AND table_name = :name", @schema, @name).first
12
+ raise NotExists if !@partitioned
13
+ @columns = oci.pluck("SELECT column_name, column_id, data_type, data_length, data_precision, data_scale, char_used, char_length " +
14
+ "FROM all_tab_columns WHERE owner = :schema AND table_name = :name ORDER BY column_id", @schema, @name).collect do |col|
15
+ Column.new(@schema, @name, col[0], id: col[1].to_i, type: col[2], length: col[3] && col[3].to_i,
16
+ precision: col[4] && col[4].to_i, scale: col[5] && col[5].to_i, char_used: col[6],
17
+ char_length: col[7] && col[7].to_i)
18
+ end
19
+ @columns_hash = Hash[@columns.collect { |col| [col.name, col] }]
20
+ self
21
+ end
22
+
23
+ def describe(options = {})
24
+ <<-HERE.reset_indentation
25
+ Table #{@schema}.#{@name}
26
+ Partitioned: #{@partitioned}
27
+ HERE
28
+ end
29
+
30
+ def list(options = {}, filter = nil)
31
+ columns = @columns_hash.keys
32
+ columns.select! { |c| c =~ /^#{Regexp.escape(filter).gsub('\*', '.*').gsub('\?', '.')}$/ } if filter
33
+ columns
34
+ end
35
+
36
+ def columns(column)
37
+ raise NotExists if !@columns_hash[column]
38
+ @columns_hash[column]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require_relative './object.rb'
2
+
3
+ module Oraora
4
+ class Meta
5
+ class View < Object
6
+ def type
7
+ 'VIEW'
8
+ end
9
+
10
+ def load_from_oci(oci)
11
+ x = oci.select_one("SELECT 1 FROM all_views WHERE owner = :schema AND view_name = :name", @schema, @name).first
12
+ raise NotExists if !x
13
+ @columns = oci.pluck("SELECT column_name, column_id, data_type, data_length, data_precision, data_scale, char_used, char_length " +
14
+ "FROM all_tab_columns WHERE owner = :schema AND table_name = :name ORDER BY column_id", @schema, @name).collect do |col|
15
+ Column.new(@schema, @name, col[0], id: col[1].to_i, type: col[2], length: col[3] && col[3].to_i,
16
+ precision: col[4] && col[4].to_i, scale: col[5] && col[5].to_i, char_used: col[6],
17
+ char_length: col[7] && col[7].to_i)
18
+ end
19
+ @columns_hash = Hash[@columns.collect { |col| [col.name, col] }]
20
+ self
21
+ end
22
+
23
+ def describe(options = {})
24
+ <<-HERE.reset_indentation
25
+ View #{@schema}.#{@name}
26
+ HERE
27
+ end
28
+
29
+ def list(options = {}, filter = nil)
30
+ columns = @columns_hash.keys
31
+ columns.select! { |c| c =~ /^#{Regexp.escape(filter).gsub('\*', '.*').gsub('\?', '.')}$/ } if filter
32
+ columns
33
+ end
34
+
35
+ def columns(column)
36
+ raise NotExists if !@columns_hash[column]
37
+ @columns_hash[column]
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/oraora/oci.rb ADDED
@@ -0,0 +1,56 @@
1
+ module Oraora
2
+ # Wrapper around OCI8 to add some extra stuff
3
+ class OCI < OCI8
4
+ # Wrapped in a separate thread as OCI8 seems to ignore interrupts
5
+ def initialize(*args)
6
+ ret = nil
7
+ thread = Thread.new { ret = super }
8
+ thread.join
9
+ ret
10
+ end
11
+
12
+ # Wrapped in a separate thread as OCI8 seems to ignore interrupts
13
+ def logoff
14
+ ret = nil
15
+ thread = Thread.new { ret = super }
16
+ thread.join
17
+ ret
18
+ end
19
+
20
+ # Wrapped in a separate thread with Interrupt handling
21
+ def exec(sql, *bindvars, &block)
22
+ ret = nil
23
+ thread = Thread.new { ret = super }
24
+ thread.join
25
+ ret
26
+ rescue Interrupt
27
+ self.break
28
+ raise
29
+ end
30
+
31
+ # Wrapped in a separate thread with Interrupt handling
32
+ def select_one(sql, *bindvars)
33
+ ret = nil
34
+ thread = Thread.new { ret = super }
35
+ thread.join
36
+ ret
37
+ rescue Interrupt
38
+ self.break
39
+ raise
40
+ end
41
+
42
+ # Returns the query result as an array of arrays
43
+ def pluck(sql, *bindvars)
44
+ result = []
45
+ exec(sql, *bindvars) { |row| result << row }
46
+ result
47
+ end
48
+
49
+ # Returns first column of a query as an array
50
+ def pluck_one(sql, *bindvars)
51
+ result = []
52
+ exec(sql, *bindvars) { |row| result << row.first }
53
+ result
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+ module Oraora
2
+ module Terminal
3
+ def self.width
4
+ HighLine::SystemExtensions.terminal_size[0]
5
+ end
6
+
7
+ def self.height
8
+ HighLine::SystemExtensions.terminal_size[1]
9
+ end
10
+
11
+ def self.puts_grid(items)
12
+ # TODO: Disable terminal size check when not reading from terminal
13
+ terminal_cols = [width || 0, 32].max
14
+ object_cols = terminal_cols / 32
15
+ # TODO: Determine optimal object_cols
16
+ num_rows = (items.length + object_cols - 1) / object_cols
17
+ #@logger.debug "Determined #{num_rows} rows of #{object_cols} objects for #{items.count} objects and #{terminal_cols} terminal width"
18
+ (0...num_rows).each do |row|
19
+ line = ''
20
+ (0...object_cols).each do |col|
21
+ index = num_rows * col + row
22
+ line += items[index].ljust(32) if items[index]
23
+ end
24
+ puts line
25
+ end
26
+ end
27
+
28
+ def self.puts_cursor(cursor)
29
+ # Column metadata
30
+ column_names = cursor.get_col_names
31
+
32
+ cursor.prefetch_rows = 1000
33
+ begin
34
+ # Fetch 1000 rows
35
+ output = []
36
+ column_lengths = Array.new(column_names.length, 1)
37
+ while output.length < 1000 && record = cursor.fetch
38
+ record.collect! { |val| val.is_a?(BigDecimal) ? val.to_s('F').gsub(/\.0+$/, '') : val.to_s }
39
+ output << record
40
+ column_lengths = column_lengths.zip(record.collect { |v| v.length}).collect(&:max)
41
+ end
42
+
43
+ # Output
44
+ if !output.empty?
45
+ puts "%-*.*s " * column_names.length % column_lengths.zip(column_lengths, column_names).flatten
46
+ puts "%-*s " * column_names.length % column_lengths.zip(column_lengths.collect { |c| '-' * c }).flatten
47
+ output.each do |row|
48
+ puts "%-*s " * row.length % column_lengths.zip(row).flatten
49
+ end
50
+ puts
51
+ end
52
+ end while record
53
+ end
54
+ end
55
+ end
data/oraora.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'oraora'
3
+ s.version = '0.1.0'
4
+ s.summary = "Interactive command-line utility for Oracle"
5
+ s.description = "Interactive command-line utility for Oracle"
6
+
7
+ s.author = "kmehkeri"
8
+ s.email = 'kmehkeri@gmail.com'
9
+ s.homepage = 'http://rubygems.org/gems/oraora'
10
+ s.license = 'MIT'
11
+
12
+ s.files = `git ls-files`.split($/)
13
+ s.executables = `git ls-files -- bin`.split($/).map { |f| File.basename(f) }
14
+
15
+ s.add_runtime_dependency 'highline'
16
+ s.add_runtime_dependency 'ruby-oci8'
17
+ s.add_runtime_dependency 'indentation'
18
+ s.add_runtime_dependency 'colorize'
19
+ s.add_runtime_dependency 'bigdecimal'
20
+
21
+ s.add_development_dependency 'rspec'
22
+ end
@@ -0,0 +1,132 @@
1
+ require 'oraora/context'
2
+
3
+ describe Oraora::Context do
4
+
5
+ describe '.initialize' do
6
+ it "should initialize from hash" do
7
+ context = Oraora::Context.new('V', schema: 'X', object: 'Y', object_type: 'TABLE')
8
+ expect( context.level ).to eql :object
9
+ expect( context.user ).to eql 'V'
10
+ expect( context.schema ).to eql 'X'
11
+ expect( context.object ).to eql 'Y'
12
+ expect( context.object_type ).to eql 'TABLE'
13
+ end
14
+ end
15
+
16
+ describe '.su' do
17
+ it "should return a different context object" do
18
+ context = Oraora::Context.new('V', schema: 'X', object: 'Y', object_type: 'TABLE')
19
+ expect( context.su('Z') ).not_to eql context
20
+ end
21
+
22
+ it "should store specified user" do
23
+ context = Oraora::Context.new('V', schema: 'X', object: 'Y', object_type: 'TABLE').su('Z')
24
+ expect( context.user ).to eql 'Z'
25
+ end
26
+
27
+ it "should store original context's data" do
28
+ context = Oraora::Context.new('V', schema: 'X', object: 'Y', object_type: 'TABLE').su('Z')
29
+ expect( context.schema ).to eql 'X'
30
+ expect( context.object ).to eql 'Y'
31
+ end
32
+ end
33
+
34
+ describe '.dup' do
35
+ it "should return equal context" do
36
+ context = Oraora::Context.new('V', schema: 'X', object: 'Y', object_type: 'TABLE').dup
37
+ expect( context.user ).to eql 'V'
38
+ expect( context.schema ).to eql 'X'
39
+ expect( context.object ).to eql 'Y'
40
+ expect( context.object_type ).to eql 'TABLE'
41
+ end
42
+ end
43
+
44
+ describe '#set & #traverse' do
45
+ it "should set root context correctly" do
46
+ context = Oraora::Context.new.set({})
47
+ expect( context.level ).to be_nil
48
+ expect( context.schema ).to be_nil
49
+ end
50
+
51
+ it "should set schema context correctly" do
52
+ context = Oraora::Context.new.set(schema: 'A')
53
+ expect( context.level ).to eql :schema
54
+ expect( context.schema ).to eql 'A'
55
+ end
56
+
57
+ it "should set column context correctly" do
58
+ context = Oraora::Context.new.set(schema: 'A', object: 'B', object_type: 'TABLE', column: 'C')
59
+ expect( context.level ).to eql :column
60
+ expect( context.schema ).to eql 'A'
61
+ expect( context.object ).to eql 'B'
62
+ expect( context.column ).to eql 'C'
63
+ end
64
+
65
+ it "should set user correctly on the context" do
66
+ context = Oraora::Context.new('AA').set(schema: 'A')
67
+ expect( context.level ).to eql :schema
68
+ expect( context.user ).to eql 'AA'
69
+ expect( context.schema ).to eql 'A'
70
+ end
71
+
72
+ it "should save user when switching context" do
73
+ context = Oraora::Context.new('AA').set(schema: 'A')
74
+ context.set(schema: 'B')
75
+ expect( context.level ).to eql :schema
76
+ expect( context.user ).to eql 'AA'
77
+ expect( context.schema ).to eql 'B'
78
+ end
79
+
80
+ it "should raise error on invalid key" do
81
+ expect { Oraora::Context.new.set(foo: 'A') }.to raise_exception(Oraora::Context::InvalidKey)
82
+ end
83
+
84
+ it "should raise error on missing object_type when object is present" do
85
+ expect { Oraora::Context.new.set(schema: 'A', object: 'B') }.to raise_exception(Oraora::Context::InvalidKey)
86
+ end
87
+
88
+ it "should raise error on invalid combination of keys" do
89
+ expect { Oraora::Context.new.set(schema: 'A', column: 'B', subprogram: 'C') }.to raise_exception(Oraora::Context::InvalidKey)
90
+ end
91
+ end
92
+
93
+ describe "#up" do
94
+ it "should do nothing when already at root" do
95
+ context = Oraora::Context.new.up
96
+ expect( context.level ).to be_nil
97
+ end
98
+
99
+ it "should go up from object to schema level" do
100
+ context = Oraora::Context.new(nil, schema: 'A', object: 'B', object_type: 'TABLE').up
101
+ expect( context.level ).to eql :schema
102
+ expect( context.schema ).to eql 'A'
103
+ expect( context.object ).to be_nil
104
+ end
105
+
106
+ it "should go up from column to object level" do
107
+ context = Oraora::Context.new(nil, schema: 'X', object: 'Y', object_type: 'TABLE', column: 'Z').up
108
+ expect( context.level ).to eql :object
109
+ expect( context.schema ).to eql 'X'
110
+ expect( context.object ).to be_eql 'Y'
111
+ expect( context.column ).to be_nil
112
+ end
113
+ end
114
+
115
+ describe '#prompt' do
116
+ it "should display correct prompt for context" do
117
+ {
118
+ {} => '/',
119
+ { schema: 'Q' } => 'Q',
120
+ { schema: 'U' } => '~',
121
+ { schema: 'A', object: 'B', object_type: 'TABLE' } => 'A.B',
122
+ { schema: 'U', object: 'B', object_type: 'PACKAGE' } => '~.B',
123
+ { schema: 'U', object: 'Y', object_type: 'TABLE', column: 'Z' } => '~.Y.Z',
124
+ { schema: 'MMM', object: 'NNN', object_type: 'VIEW', column: 'TEST' } => 'MMM.NNN.TEST'
125
+ }.each do |hash, prompt|
126
+ expect( Oraora::Context.new('U', hash).prompt ).to eql prompt
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+