rsql 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +26 -15
- data/TODO +8 -2
- data/bin/rsql +2 -2
- data/example.rsqlrc +148 -0
- data/lib/rsql/commands.rb +39 -31
- data/lib/rsql/eval_context.rb +8 -8
- data/lib/rsql/mysql_results.rb +0 -0
- data/lib/rsql.rb +1 -1
- metadata +8 -7
data/README.txt
CHANGED
@@ -11,7 +11,7 @@ for access to the SQL server.
|
|
11
11
|
|
12
12
|
== SYNOPSIS
|
13
13
|
|
14
|
-
|
14
|
+
Run rsql with no arguments for usage.
|
15
15
|
|
16
16
|
Aside from the standard MySQL command syntax, the following
|
17
17
|
functionality allows for a little more expressive processing.
|
@@ -28,10 +28,10 @@ final result of evaluating the command string is another string, it is
|
|
28
28
|
executed as SQL. Any semicolons meant to be processed by Ruby must be
|
29
29
|
escaped. Example:
|
30
30
|
|
31
|
-
rsql> . puts 'hello world!'
|
31
|
+
rsql> . puts 'hello world!' \; 'select * from Account';
|
32
32
|
|
33
|
-
Utilizing Canned Methods
|
34
|
-
|
33
|
+
Utilizing Canned Methods (aka "Recipes")
|
34
|
+
----------------------------------------
|
35
35
|
|
36
36
|
Commands can be stored in the .rsqlrc file in your HOME directory to
|
37
37
|
expose methods that may be invoked to generate SQL with variable
|
@@ -41,23 +41,23 @@ approach. These can then be called in the same way as above. Example:
|
|
41
41
|
In the .rsqlrc file...
|
42
42
|
|
43
43
|
register :users_by_email, :email %q{
|
44
|
-
SELECT * FROM Users WHERE email = '
|
44
|
+
SELECT * FROM Users WHERE email = '#{email}'
|
45
45
|
}
|
46
46
|
|
47
47
|
...then from the prompt:
|
48
48
|
|
49
|
-
rsql> . users_by_email 'brad@gigglewax.com'
|
49
|
+
rsql> . users_by_email 'brad@gigglewax.com';
|
50
50
|
|
51
51
|
If a block is provided to the registration, it will be called as a
|
52
52
|
method. Example:
|
53
53
|
|
54
54
|
In the .sqlrc file...
|
55
55
|
|
56
|
-
register :
|
56
|
+
register :dummy, :hello do |*args|
|
57
57
|
p args
|
58
58
|
end
|
59
59
|
|
60
|
-
rsql> .
|
60
|
+
rsql> . dummy :world;
|
61
61
|
|
62
62
|
All registered methods can be listed using the built-in 'list'
|
63
63
|
command.
|
@@ -75,7 +75,7 @@ converting it into a more readable value. A bang indicator (exlamation
|
|
75
75
|
point: !) is used to demarcate a mapping of column names to Ruby
|
76
76
|
methods that should be invoked to processes content. Example:
|
77
77
|
|
78
|
-
rsql> select IpAddress from Devices
|
78
|
+
rsql> select IpAddress from Devices ! IpAddress => bin_to_str;
|
79
79
|
|
80
80
|
This will call 'bin_to_str' for each 'IpAddress' returned from the
|
81
81
|
query. Mulitple mappings are separated by a comma. These mappings can
|
@@ -87,13 +87,24 @@ Redirection
|
|
87
87
|
-----------
|
88
88
|
|
89
89
|
Output from one or more queries may be post-processed dynamically. If
|
90
|
-
any set of commands is follwed by a
|
91
|
-
will be
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
any set of commands is follwed by a pipe symbol, results from the
|
91
|
+
previous command will be available in a member variable as
|
92
|
+
@results. The results can be used through any of the MySQLResults
|
93
|
+
class' public methods (e.g. each_hash) and the final Ruby code will be
|
94
|
+
evaluated with access to them. Any result of the evaluation that is a
|
95
|
+
string is then executed as SQL. Example:
|
95
96
|
|
96
|
-
rsql> select * from
|
97
|
+
rsql> select * from Users | @results.each_hash{|r| p r};
|
98
|
+
|
99
|
+
And again, it's most useful to encapsulate the processing logic within
|
100
|
+
a registered ruby block that can be called after the pipe as a single
|
101
|
+
command.
|
102
|
+
|
103
|
+
Even More!
|
104
|
+
----------
|
105
|
+
|
106
|
+
For additional examples and functionality, read and try the
|
107
|
+
example.rsqlrc file.
|
97
108
|
|
98
109
|
== LICENSE
|
99
110
|
|
data/TODO
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
* Support separation of semicolon content from registered sql.
|
2
|
-
|
3
1
|
* Fix multiline dump to be columnar sensitive (i.e. all but the first
|
4
2
|
line will need a prefix of previous column length whitespace.
|
5
3
|
|
@@ -28,3 +26,11 @@
|
|
28
26
|
|
29
27
|
* Move last_command logic into mysql_results and remember last five
|
30
28
|
(or so).
|
29
|
+
|
30
|
+
* Fix need for SSH password! It never asks.
|
31
|
+
|
32
|
+
* Find out if it's easy to point at source from the wiki to point
|
33
|
+
people at the example.rsqlrc.
|
34
|
+
|
35
|
+
* Consider adding the option to specify a dispalyer when registering a
|
36
|
+
recipe.
|
data/bin/rsql
CHANGED
@@ -56,7 +56,7 @@ else
|
|
56
56
|
rc_fn = File.join(ENV['HOME'], ".#{bn}rc")
|
57
57
|
end
|
58
58
|
|
59
|
-
eval_context.load(rc_fn) if File.exists?(rc_fn)
|
59
|
+
eval_context.load(rc_fn, false) if File.exists?(rc_fn)
|
60
60
|
|
61
61
|
def get_password(prompt)
|
62
62
|
iswin = nil != (RUBY_PLATFORM =~ /(win|w)32$/)
|
@@ -256,7 +256,7 @@ rescue Mysql::Error => ex
|
|
256
256
|
exit 1
|
257
257
|
end
|
258
258
|
|
259
|
-
eval_context.call_init_registrations
|
259
|
+
eval_context.call_init_registrations
|
260
260
|
|
261
261
|
history_fn = File.join(ENV['HOME'], ".#{bn}_history")
|
262
262
|
if File.exists?(history_fn) && 0 < File.size(history_fn)
|
data/example.rsqlrc
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# -*- Mode: ruby -*-
|
2
|
+
|
3
|
+
# This file is meant to be a working illustration of how RSQL might be
|
4
|
+
# used and to show off various features of the application.
|
5
|
+
|
6
|
+
# All examples below will use this temporary table. You will need to
|
7
|
+
# "use" a database first before loading this file since it will need
|
8
|
+
# to create this temporary table.
|
9
|
+
#
|
10
|
+
@rsql_table = 'rsql_example'
|
11
|
+
|
12
|
+
# To use this file, change directory to the one containing this file,
|
13
|
+
# run rsql connecting to your MySQL server (run rsql with no arguments
|
14
|
+
# for usage). At the RSQL prompt, type ".load 'example.rsqlrc';"
|
15
|
+
# (without the double quotes).
|
16
|
+
|
17
|
+
# After it's loaded try out the command ".list;" (without the
|
18
|
+
# quotes).
|
19
|
+
|
20
|
+
# If you make changes to the example to try out new things (and please
|
21
|
+
# do!), you can simply enter ".reload;" (without the double quotes) to
|
22
|
+
# have your changes pulled in immediately without exiting your
|
23
|
+
# session.
|
24
|
+
|
25
|
+
################################################################################
|
26
|
+
|
27
|
+
# This type of registration is automatically invoked when this file is
|
28
|
+
# loaded. Often, this is useful to run set up routines like setting
|
29
|
+
# mysql variables for different read levels (e.g. SET SESSION
|
30
|
+
# TRANSACTION ISOLATION LEVEL READ COMMITTED). Any number of these may
|
31
|
+
# be defined.
|
32
|
+
#
|
33
|
+
# Here we are merely setting up the example table.
|
34
|
+
#
|
35
|
+
register_init :setup_example, %q{
|
36
|
+
CREATE TEMPORARY TABLE IF NOT EXISTS #{@rsql_table} (
|
37
|
+
name VARCHAR(100),
|
38
|
+
value INT(11)
|
39
|
+
)
|
40
|
+
}, :desc => 'Sets up example table for trying out RSQL.'
|
41
|
+
|
42
|
+
# This recipe is simply building up a string with a single variable
|
43
|
+
# interpolated into it (our table name). The string will then be used
|
44
|
+
# as if typed at the command line.
|
45
|
+
#
|
46
|
+
# In this case, we are simply dropping the table created by our
|
47
|
+
# initialization recipe.
|
48
|
+
#
|
49
|
+
register :cleanup, %q{
|
50
|
+
DROP TEMPORARY TABLE IF EXISTS #{@rsql_table}
|
51
|
+
}, :desc => 'Cleans up the example table.'
|
52
|
+
|
53
|
+
# This is an example of a recipe that utilizes a Ruby block for
|
54
|
+
# running code to generate the SQL we eventually return.
|
55
|
+
#
|
56
|
+
# Here we are just populating the table (if it isn't already).
|
57
|
+
#
|
58
|
+
# Notice that we are also making use of one of EvalContext's helper
|
59
|
+
# methods, "squeeze!" to make the SQL compact.
|
60
|
+
#
|
61
|
+
register :fill_table, :desc => 'Populate the example table.' do
|
62
|
+
sql = ''
|
63
|
+
10.times do |i|
|
64
|
+
sql << "
|
65
|
+
INSERT IGNORE INTO #{@rsql_table}
|
66
|
+
SET name='fancy#{i}',
|
67
|
+
value=#{i**i};
|
68
|
+
"
|
69
|
+
end
|
70
|
+
sqeeze!(sql)
|
71
|
+
end
|
72
|
+
|
73
|
+
# After you have a table with content in it, you can run queries
|
74
|
+
# against it and have the contents changed into something a little
|
75
|
+
# more meaningful. For example, what if the values in our table were
|
76
|
+
# bytes that we wanted to humanize? Try this command:
|
77
|
+
#
|
78
|
+
# rsql> select * from rsql_example ! value => humanize_bytes;
|
79
|
+
#
|
80
|
+
# The humanize_bytes method is a helper in the EvalContext
|
81
|
+
# class. There are several others avaialble. Check out the rdoc for
|
82
|
+
# details.
|
83
|
+
#
|
84
|
+
# You can also declare these column mappings in your recipes too,
|
85
|
+
# though the syntax is slightly different, using Ruby symbols.
|
86
|
+
#
|
87
|
+
register :show_values_as_bytes, %q{
|
88
|
+
SELECT value FROM #{@rsql_table}
|
89
|
+
}, 'value' => :humanize_bytes,
|
90
|
+
:desc => 'Show values as humanized bytes.'
|
91
|
+
|
92
|
+
# It is even possible to make up your own column mapping helpers. Just
|
93
|
+
# create a Ruby method and reference it as a symbol mapped to whatever
|
94
|
+
# column the helper is expecting for content. The return of the helper
|
95
|
+
# will be replaced as the column entry's content. Your method is
|
96
|
+
# called once for each value in the column from the results.
|
97
|
+
#
|
98
|
+
# Make sure if your method doesn't understand the content passed to it
|
99
|
+
# that it just reflects it back out so you don't lose data when
|
100
|
+
# printed.
|
101
|
+
#
|
102
|
+
def pretty_names(name)
|
103
|
+
if name =~ /^(\w+)(\d+)$/
|
104
|
+
"#{$1} (#{$2})"
|
105
|
+
else
|
106
|
+
name
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
register :show_pretty_names, %q{
|
111
|
+
SELECT name FROM #{@rsql_table}
|
112
|
+
}, 'name' => :pretty_names,
|
113
|
+
:desc => 'Show names separated to be more readable.'
|
114
|
+
|
115
|
+
|
116
|
+
# It's also possible to work with the full set of query results in a
|
117
|
+
# recipe. This can be useful if there is some coordination necessary
|
118
|
+
# across multiple columns to result in some new kind of report. Much
|
119
|
+
# like a shell's ability to pipe output from one command to the next,
|
120
|
+
# RSQL takes a similar approch. Try this:
|
121
|
+
#
|
122
|
+
# rsql> select * from rsql_example | p @results;
|
123
|
+
#
|
124
|
+
# The EvalContext manages the results from a previous query in the
|
125
|
+
# @results member variable accessible by any Ruby recipe code. This is
|
126
|
+
# an instance of the MySQLResults class. Below we make use of the
|
127
|
+
# each_hash method to walk over all rows. There are other helpful
|
128
|
+
# routines available as well that are documented in rdoc.
|
129
|
+
#
|
130
|
+
# Here's an example that writes a simple report of the data we are
|
131
|
+
# working with. To try this out, enter the following at the prompt:
|
132
|
+
#
|
133
|
+
# rsql> select * from rsql_example | to_report;
|
134
|
+
#
|
135
|
+
register :to_report do
|
136
|
+
small_cnt = 0
|
137
|
+
big_cnt = 0
|
138
|
+
@results.each_hash do |row|
|
139
|
+
if row['value'].to_i < 10000
|
140
|
+
small_cnt +=1
|
141
|
+
else
|
142
|
+
big_cnt += 1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
puts "There are #{small_cnt} small values and #{big_cnt} big values."
|
146
|
+
end
|
147
|
+
|
148
|
+
# vi: set filetype=ruby
|
data/lib/rsql/commands.rb
CHANGED
@@ -118,6 +118,14 @@ module RSQL
|
|
118
118
|
return @cmds.empty?
|
119
119
|
end
|
120
120
|
|
121
|
+
def concat(other)
|
122
|
+
@cmds.concat(other)
|
123
|
+
end
|
124
|
+
|
125
|
+
def last
|
126
|
+
@cmds.last
|
127
|
+
end
|
128
|
+
|
121
129
|
def run!(eval_context)
|
122
130
|
last_results = nil
|
123
131
|
while @cmds.any?
|
@@ -150,9 +158,6 @@ module RSQL
|
|
150
158
|
when ?.
|
151
159
|
content.slice!(0)
|
152
160
|
declarator = :ruby
|
153
|
-
when ?@
|
154
|
-
content.slice!(0)
|
155
|
-
declarator = :iterator
|
156
161
|
else
|
157
162
|
declarator = is_ruby ? :ruby : nil
|
158
163
|
end
|
@@ -176,39 +181,42 @@ module RSQL
|
|
176
181
|
end
|
177
182
|
|
178
183
|
def run_command(cmd, last_results, eval_context)
|
179
|
-
|
180
|
-
|
181
|
-
# set up to allow an iterator to run up to 100,000 times
|
182
|
-
100000.times do |i|
|
183
|
-
eval_context.bangs = cmd.bangs
|
184
|
-
|
185
|
-
if cmd.declarator
|
186
|
-
ctx.index = i
|
187
|
-
ctx.last_results = last_results
|
188
|
-
stdout = cmd.displayer == :pipe ? StringIO.new : nil
|
189
|
-
value = eval_context.safe_eval(cmd.content, ctx, stdout)
|
190
|
-
else
|
191
|
-
value = cmd.content
|
192
|
-
end
|
193
|
-
|
194
|
-
return :done if value == 'exit' || value == 'quit'
|
184
|
+
eval_context.bangs = cmd.bangs
|
195
185
|
|
186
|
+
if cmd.declarator
|
187
|
+
stdout = cmd.displayer == :pipe ? StringIO.new : nil
|
188
|
+
value = eval_context.safe_eval(cmd.content, last_results, stdout)
|
196
189
|
if String === value
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
raise
|
190
|
+
cmds = Commands.new(value, @default_displayer)
|
191
|
+
unless cmds.empty?
|
192
|
+
# need to carry along the bangs into the
|
193
|
+
# last command so we don't lose them
|
194
|
+
if cmds.last.bangs.empty? && cmd.bangs.any?
|
195
|
+
cmds.last.bangs = cmd.bangs
|
196
|
+
end
|
197
|
+
@cmds = cmds.concat(@cmds)
|
206
198
|
end
|
207
|
-
|
208
|
-
last_results = EvalResults.new(value, stdout)
|
199
|
+
return
|
209
200
|
end
|
201
|
+
else
|
202
|
+
value = cmd.content
|
203
|
+
end
|
210
204
|
|
211
|
-
|
205
|
+
return :done if value == 'exit' || value == 'quit'
|
206
|
+
|
207
|
+
if String === value
|
208
|
+
begin
|
209
|
+
last_results = MySQLResults.query(value, eval_context)
|
210
|
+
rescue MySQLResults::MaxRowsException => ex
|
211
|
+
$stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})"
|
212
|
+
rescue MysqlError => ex
|
213
|
+
$stderr.puts ex.message
|
214
|
+
rescue Exception => ex
|
215
|
+
$stderr.puts ex.inspect
|
216
|
+
raise
|
217
|
+
end
|
218
|
+
else
|
219
|
+
last_results = EvalResults.new(value, stdout)
|
212
220
|
end
|
213
221
|
|
214
222
|
return last_results
|
data/lib/rsql/eval_context.rb
CHANGED
@@ -28,13 +28,12 @@ module RSQL
|
|
28
28
|
|
29
29
|
Registration = Struct.new(:name, :args, :bangs, :block, :usage, :desc)
|
30
30
|
|
31
|
-
CommandContext = Struct.new(:index, :incomplete, :last_results, :state)
|
32
|
-
|
33
31
|
HEXSTR_LIMIT = 32
|
34
32
|
|
35
33
|
def initialize
|
36
34
|
@hexstr_limit = HEXSTR_LIMIT
|
37
35
|
@last_cmd = nil
|
36
|
+
@results = nil
|
38
37
|
|
39
38
|
@loaded_fns = []
|
40
39
|
@init_registrations = []
|
@@ -66,15 +65,15 @@ module RSQL
|
|
66
65
|
|
67
66
|
attr_accessor :bangs
|
68
67
|
|
69
|
-
def call_init_registrations
|
68
|
+
def call_init_registrations
|
70
69
|
@init_registrations.each do |sym|
|
71
70
|
reg = @registrations[sym]
|
72
71
|
sql = reg.block.call(*reg.args)
|
73
|
-
|
72
|
+
query(sql) if String === sql
|
74
73
|
end
|
75
74
|
end
|
76
75
|
|
77
|
-
def load(fn)
|
76
|
+
def load(fn, init=true)
|
78
77
|
ret = Thread.new {
|
79
78
|
begin
|
80
79
|
eval(File.read(fn), binding, fn)
|
@@ -89,11 +88,12 @@ module RSQL
|
|
89
88
|
$stderr.puts("#{ret.class}: #{ret.message}", bt, '')
|
90
89
|
else
|
91
90
|
@loaded_fns << fn unless @loaded_fns.include?(fn)
|
91
|
+
call_init_registrations if init
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
95
|
def reload
|
96
|
-
@loaded_fns.each{|fn| self.load(fn)}
|
96
|
+
@loaded_fns.each{|fn| self.load(fn, false)}
|
97
97
|
puts "loaded: #{@loaded_fns.inspect}"
|
98
98
|
end
|
99
99
|
|
@@ -111,8 +111,8 @@ module RSQL
|
|
111
111
|
|
112
112
|
# safely evaluate Ruby content within our context
|
113
113
|
#
|
114
|
-
def safe_eval(content,
|
115
|
-
@
|
114
|
+
def safe_eval(content, results, stdout)
|
115
|
+
@results = results
|
116
116
|
|
117
117
|
# allow a simple reload to be called directly as it requires a
|
118
118
|
# little looser safety valve...
|
data/lib/rsql/mysql_results.rb
CHANGED
File without changes
|
data/lib/rsql.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rsql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 4
|
10
|
+
version: 0.1.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Brad Robel-Forrest
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-05-
|
18
|
+
date: 2011-05-15 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: net-ssh
|
@@ -39,8 +39,8 @@ description: |
|
|
39
39
|
connection to an intermediary host for access to the MySQL server.
|
40
40
|
|
41
41
|
email: brad+rsql@gigglewax.com
|
42
|
-
executables:
|
43
|
-
|
42
|
+
executables:
|
43
|
+
- rsql
|
44
44
|
extensions: []
|
45
45
|
|
46
46
|
extra_rdoc_files: []
|
@@ -50,6 +50,7 @@ files:
|
|
50
50
|
- README.txt
|
51
51
|
- TODO
|
52
52
|
- bin/rsql
|
53
|
+
- example.rsqlrc
|
53
54
|
- lib/rsql.rb
|
54
55
|
- lib/rsql/commands.rb
|
55
56
|
- lib/rsql/eval_context.rb
|
@@ -86,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
87
|
requirements: []
|
87
88
|
|
88
89
|
rubyforge_project:
|
89
|
-
rubygems_version: 1.
|
90
|
+
rubygems_version: 1.8.2
|
90
91
|
signing_key:
|
91
92
|
specification_version: 3
|
92
93
|
summary: Ruby based MySQL command line with recipes.
|