mssql 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|