mssql 0.0.2
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.
- data/.autotest +10 -0
- data/.gitignore +2 -0
- data/.rbenv-version +1 -0
- data/Gemfile +16 -0
- data/LICENSE +22 -0
- data/README.md +190 -0
- data/Rakefile +9 -0
- data/bin/mssql +7 -0
- data/emacs/sql-ms.el +120 -0
- data/lib/command.rb +133 -0
- data/lib/command_parser.rb +28 -0
- data/lib/connection.rb +58 -0
- data/lib/controller.rb +126 -0
- data/lib/mssql.rb +22 -0
- data/lib/params_parser.rb +49 -0
- data/lib/query_output.rb +55 -0
- data/lib/simple_update.in +50 -0
- data/lib/simple_update.sql +50 -0
- data/lib/table_output.rb +71 -0
- data/lib/version.rb +3 -0
- data/mssql.gemspec +21 -0
- data/test/in_out/authors.actual +29 -0
- data/test/in_out/authors.expected +29 -0
- data/test/in_out/authors.sql +1 -0
- data/test/in_out/authors_update.actual +1 -0
- data/test/in_out/authors_update.expected +1 -0
- data/test/in_out/authors_update.sql +1 -0
- data/test/in_out/sp_help.actual +74 -0
- data/test/in_out/sp_help.expected +74 -0
- data/test/in_out/sp_help.sql +1 -0
- data/test/test_command_parser.rb +25 -0
- data/test/test_connection.rb +103 -0
- data/test/test_helper.rb +48 -0
- data/test/test_input_output.rb +68 -0
- data/test/test_table.rb +26 -0
- data/todo +20 -0
- metadata +129 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
class CommandParser
|
2
|
+
|
3
|
+
def initialize(line)
|
4
|
+
@line = line
|
5
|
+
@command = nil
|
6
|
+
@params = nil
|
7
|
+
parse
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :command, :params
|
11
|
+
|
12
|
+
def is_command?
|
13
|
+
!@command.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
m = @line.match /^\s*\.(\w+) *(.*)$/
|
18
|
+
return if m.nil?
|
19
|
+
@command = m[1].to_sym
|
20
|
+
@params = m[2].split(' ').compact.map{|p| p.split(".")}.flatten
|
21
|
+
case @command
|
22
|
+
when "find"
|
23
|
+
when "explain"
|
24
|
+
when "use"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/lib/connection.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
class Connection
|
2
|
+
|
3
|
+
def initialize(configs, after_connect_handler=nil)
|
4
|
+
@configs = configs
|
5
|
+
@after_connect_handler = after_connect_handler
|
6
|
+
use :default_connection
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :results, :error, :name
|
10
|
+
|
11
|
+
def error?
|
12
|
+
!@error.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def one_column_one_row?
|
16
|
+
return false if error?
|
17
|
+
@results.rows.size == 1 && @results.columns.size == 1
|
18
|
+
rescue
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def exec(sql)
|
23
|
+
begin
|
24
|
+
result = @client.execute sql
|
25
|
+
rows = result.each(:symbolize_keys => true, :empty_sets => true, :as => :array)
|
26
|
+
@results = Hashie::Mash.new({
|
27
|
+
:columns => result.fields,
|
28
|
+
:rows => rows,
|
29
|
+
:affected => result.do,
|
30
|
+
:return_code => @client.return_code
|
31
|
+
})
|
32
|
+
@error = nil
|
33
|
+
@results
|
34
|
+
rescue TinyTds::Error => e
|
35
|
+
@result = nil
|
36
|
+
@error = Hashie::Mash.new(:error => e.to_s, :severity => e.severity, :db_error_number => e.db_error_number)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
KEYS = [:username, :password, :host, :database]
|
41
|
+
|
42
|
+
def use(name = :default_connection)
|
43
|
+
return false unless @configs.has_key?(name.to_s)
|
44
|
+
connect @configs[name.to_s]
|
45
|
+
return true
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def connect(config)
|
51
|
+
new_client = TinyTds::Client.new(config.to_hash.symbolize_keys)
|
52
|
+
@client.close if @client
|
53
|
+
@client = new_client
|
54
|
+
@name = config.name || "#{config.username}@#{config.host}"
|
55
|
+
@after_connect_handler.call(@name) if @after_connect_handler
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/controller.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
class Controller
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
connect
|
7
|
+
trap_int
|
8
|
+
@lines = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
if ENV["EMACS"] == "t"
|
13
|
+
emacs_run_loop
|
14
|
+
elsif @options.input_file
|
15
|
+
show File.read(@options.input_file)
|
16
|
+
elsif @options.query
|
17
|
+
show @options.query
|
18
|
+
else
|
19
|
+
run_loop
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def emacs_run_loop
|
26
|
+
print @prompt
|
27
|
+
$stdin.flush
|
28
|
+
$stdin.each_line do |line|
|
29
|
+
#print "emacs_run_loop #{line}"
|
30
|
+
ret = handle_line(line)
|
31
|
+
print @prompt unless has_more?
|
32
|
+
$stdout.fsync
|
33
|
+
break if ret && ret < 0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_more?
|
38
|
+
c = $stdin.read_nonblock(1) rescue nil
|
39
|
+
$stdin.ungetbyte(c) if c
|
40
|
+
!!c
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_loop
|
44
|
+
loop do
|
45
|
+
while line = Readline.readline(@prompt, true)
|
46
|
+
ret = handle_line(line)
|
47
|
+
return if ret && ret < 0
|
48
|
+
end
|
49
|
+
exec_lines
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_line(line)
|
54
|
+
line = line.strip
|
55
|
+
return if line.empty?
|
56
|
+
command = Command.new(line, @connection)
|
57
|
+
if command.exit?
|
58
|
+
exec_lines
|
59
|
+
return -1
|
60
|
+
end
|
61
|
+
if command.go?
|
62
|
+
exec_lines
|
63
|
+
end
|
64
|
+
return 0 if command.exec
|
65
|
+
@lines << line
|
66
|
+
# if $stdin.eof?
|
67
|
+
# exec_lines
|
68
|
+
# return -1
|
69
|
+
# end
|
70
|
+
0
|
71
|
+
end
|
72
|
+
|
73
|
+
def exec_lines
|
74
|
+
show @lines.join("\n")
|
75
|
+
@lines = []
|
76
|
+
end
|
77
|
+
|
78
|
+
def trap_int
|
79
|
+
#stty_save = `stty -g`.chomp
|
80
|
+
trap('INT') do
|
81
|
+
#system('stty', stty_save);
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def show(query)
|
87
|
+
QueryOutput.new(@connection, query).show
|
88
|
+
end
|
89
|
+
|
90
|
+
def connect
|
91
|
+
read_configs
|
92
|
+
@connection = Connection.new @configs, Proc.new{ |name| @prompt = "#{name}> " }
|
93
|
+
rescue TinyTds::Error => e
|
94
|
+
print "#{e.to_s}\n"
|
95
|
+
exit
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_configs
|
99
|
+
file_configs = YAML.load(File.read("#{ENV['HOME']}/.mssql")) rescue {}
|
100
|
+
@configs = Hashie::Mash.new(file_configs)
|
101
|
+
params = ParamsParser.new
|
102
|
+
@options = params.options
|
103
|
+
unless params.options.empty?
|
104
|
+
if params.options.connection
|
105
|
+
key = params.options.connection.to_s
|
106
|
+
if @configs.has_key?(key)
|
107
|
+
@configs.default_connection = @configs[key]
|
108
|
+
else
|
109
|
+
print "unkonwn connection #{key}\n"
|
110
|
+
exit 1
|
111
|
+
end
|
112
|
+
else
|
113
|
+
if missing = params.missing_key(Connection::KEYS)
|
114
|
+
print "missing #{missing} param\n"
|
115
|
+
exit 2
|
116
|
+
end
|
117
|
+
@configs.default_connection = params.options
|
118
|
+
end
|
119
|
+
end
|
120
|
+
unless @configs.default_connection
|
121
|
+
params.print_usage
|
122
|
+
exit 3
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
data/lib/mssql.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'pp'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
#require "bundler/setup"
|
7
|
+
|
8
|
+
require 'tiny_tds'
|
9
|
+
require 'hashie'
|
10
|
+
require 'active_support/core_ext/hash/keys.rb'
|
11
|
+
|
12
|
+
$: << File.join(File.dirname(__FILE__))
|
13
|
+
|
14
|
+
require 'params_parser'
|
15
|
+
require 'connection'
|
16
|
+
require 'table_output'
|
17
|
+
require 'command'
|
18
|
+
require 'controller'
|
19
|
+
require 'query_output'
|
20
|
+
require 'command_parser'
|
21
|
+
|
22
|
+
STDOUT.sync = true
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class ParamsParser
|
2
|
+
|
3
|
+
def initialize
|
4
|
+
@options = Hashie::Mash.new
|
5
|
+
parse
|
6
|
+
end
|
7
|
+
|
8
|
+
def parse
|
9
|
+
@opts = OptionParser.new do |opts|
|
10
|
+
opts.banner = ""
|
11
|
+
available_options = [
|
12
|
+
['-c', 'connection', 'use connection defined in ~/.mssql'],
|
13
|
+
['-h', 'host', 'server host'],
|
14
|
+
['-u', 'username', 'username'],
|
15
|
+
['-p', 'password', 'password'],
|
16
|
+
['-d', 'database', 'use database name'],
|
17
|
+
['-i', 'input_file', 'input file name'],
|
18
|
+
['-q', 'query', 'run query and exit']
|
19
|
+
]
|
20
|
+
available_options.each do |o|
|
21
|
+
opts.on(o[0], "--#{o[1]} #{o[1].upcase}", o[2]) do |value|
|
22
|
+
@options[o[1]] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
opts.on_tail("-?", "--help", "show syntax summary") do
|
26
|
+
print_usage
|
27
|
+
exit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@opts
|
31
|
+
@opts.parse!(ARGV)
|
32
|
+
@opts
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_usage
|
36
|
+
puts <<-END
|
37
|
+
Usage: #{File.basename($0)} <options>
|
38
|
+
#{@opts.to_s}
|
39
|
+
END
|
40
|
+
end
|
41
|
+
|
42
|
+
def missing_key(keys)
|
43
|
+
keys.find do |key|
|
44
|
+
!@options.has_key?(key.to_s)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :options
|
49
|
+
end
|
data/lib/query_output.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class QueryOutput
|
2
|
+
|
3
|
+
def initialize(connection, query)
|
4
|
+
@connection = connection
|
5
|
+
@query = query
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
exec
|
10
|
+
@connection.error? ? print_error : print_tables
|
11
|
+
end
|
12
|
+
|
13
|
+
def show_text_or_table
|
14
|
+
exec
|
15
|
+
if @connection.one_column_one_row?
|
16
|
+
print_text
|
17
|
+
else
|
18
|
+
print_tables unless @connection.error?
|
19
|
+
end
|
20
|
+
print_error if @connection.error?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def print_error
|
26
|
+
error = @connection.error
|
27
|
+
print "\nmsg: #{error.db_error_number}, severity: #{error.severity}\n#{error.error}\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_text
|
31
|
+
print @connection.results.rows[0][0]
|
32
|
+
print "\n"
|
33
|
+
rescue
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_tables
|
37
|
+
results = @connection.results
|
38
|
+
if results.columns.first.kind_of?(Array)
|
39
|
+
(0..results.columns.size-1).each do |index|
|
40
|
+
print TableOutput.new(results.columns[index], results.rows[index]).to_s
|
41
|
+
end
|
42
|
+
else
|
43
|
+
if results.columns.size > 0
|
44
|
+
print TableOutput.new(results.columns, results.rows).to_s
|
45
|
+
else
|
46
|
+
print "#{results.affected} rows affected\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def exec
|
52
|
+
@connection.exec @query
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class TableOutput
|
2
|
+
|
3
|
+
def initialize(cols, rows)
|
4
|
+
@cols = cols
|
5
|
+
@rows = rows
|
6
|
+
calc_sizes
|
7
|
+
@ascii_encoding = Encoding.find("ASCII-8BIT")
|
8
|
+
end
|
9
|
+
|
10
|
+
def calc_sizes
|
11
|
+
@sizes = []
|
12
|
+
@cols.each_with_index do |col, index|
|
13
|
+
@sizes[index] = col.to_s.length
|
14
|
+
end
|
15
|
+
@rows.each do |row|
|
16
|
+
data_row = []
|
17
|
+
row.each_with_index do |value, index|
|
18
|
+
# TODO - fix for binary data (timestams)
|
19
|
+
if value.class == String && value.encoding.to_s == "ASCII-8BIT"
|
20
|
+
value = "***"
|
21
|
+
row[index] = value
|
22
|
+
end
|
23
|
+
size = value.to_s.length
|
24
|
+
if @sizes[index] < size
|
25
|
+
@sizes[index] = size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
format = @sizes.map{|s| " %-#{s}s "}.join("|")
|
33
|
+
output = []
|
34
|
+
|
35
|
+
separator = @sizes.map{|s| "-" * (s+2)}.join("+")
|
36
|
+
separator = "+#{separator}+"
|
37
|
+
output << ""
|
38
|
+
output << separator
|
39
|
+
output << "|#{format}|" % @cols
|
40
|
+
output << separator
|
41
|
+
@rows.each do |row|
|
42
|
+
output << "|#{format}|" % row
|
43
|
+
end
|
44
|
+
output << separator
|
45
|
+
output << "#{@rows.size} rows affected"
|
46
|
+
output << ""
|
47
|
+
output.join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class TableOutput
|
2
|
+
|
3
|
+
def initialize(cols, rows)
|
4
|
+
@cols = cols
|
5
|
+
@rows = rows
|
6
|
+
calc_sizes
|
7
|
+
@ascii_encoding = Encoding.find("ASCII-8BIT")
|
8
|
+
end
|
9
|
+
|
10
|
+
def calc_sizes
|
11
|
+
@sizes = []
|
12
|
+
@cols.each_with_index do |col, index|
|
13
|
+
@sizes[index] = col.to_s.length
|
14
|
+
end
|
15
|
+
@rows.each do |row|
|
16
|
+
data_row = []
|
17
|
+
row.each_with_index do |value, index|
|
18
|
+
# TODO - fix for binary data (timestams)
|
19
|
+
if value.class == String && value.encoding.to_s == "ASCII-8BIT"
|
20
|
+
value = "***"
|
21
|
+
row[index] = value
|
22
|
+
end
|
23
|
+
size = value.to_s.length
|
24
|
+
if @sizes[index] < size
|
25
|
+
@sizes[index] = size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
format = @sizes.map{|s| " %-#{s}s "}.join("|")
|
33
|
+
output = []
|
34
|
+
|
35
|
+
separator = @sizes.map{|s| "-" * (s+2)}.join("+")
|
36
|
+
separator = "+#{separator}+"
|
37
|
+
output << ""
|
38
|
+
output << separator
|
39
|
+
output << "|#{format}|" % @cols
|
40
|
+
output << separator
|
41
|
+
@rows.each do |row|
|
42
|
+
output << "|#{format}|" % row
|
43
|
+
end
|
44
|
+
output << separator
|
45
|
+
output << "#{@rows.size} rows affected"
|
46
|
+
output << ""
|
47
|
+
output.join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|