rsql 0.1.3 → 0.1.4
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/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.
|