cetusql 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 40c861766945ebcd5aeb412cff9788e0cf0a6292
4
+ data.tar.gz: 960a09544ab79c76a9718972240b5b470e2e6d91
5
+ SHA512:
6
+ metadata.gz: 241637ec8ca55d51a70060827a87694550ba44d9e48a5397a807af399e20b9f264a77bbd770f77aceced890ff2f6dfe6994a3ebc404a19cc7b19a03dac48d95b
7
+ data.tar.gz: c56e7c267813b5a71578dcb7dd16a164cf6825cae3b6253199bc574036de38c1b8959d19eb84cbedcffb053179be5f04b1286ddb1e446347e9f74365b8580063
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.tgz
11
+ *.bak
12
+ t.t
13
+ *.tmp
14
+ *.t
15
+ *.sqlite
16
+ *.db
17
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cetusql.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 rkumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Cetusql
2
+
3
+ Command-line based sqlite3 database explorer. The idea is to minimize typing of basic queries/tablenames and column names by allowing for selection of the same.
4
+
5
+
6
+ Some basic features:
7
+
8
+ - Lists databases in current folder and allows menu for selection.
9
+ - selection of tablenames from list, and of columns names and creates formatted output to a local temp file which
10
+ is then displayed.
11
+ - bookmark/save common SQL queries and recall later.
12
+
13
+ Uses sqlite3 gem, readline for editing. In the case of columns, uses `fzf` for selection.
14
+
15
+ Also, uses 'term-table.rb' for formatting of output. This needs to be placed in your path as an executable.
16
+
17
+ You may replace that with your own formatter such as `csvlook` or `column -t`. However, these two crash on non-ascii characters on a Mac, so I've written a ruby replacement.
18
+ You can forgo it entirely and just have comma or tab separated output sent to a file.
19
+
20
+ ## Installation
21
+
22
+ $ gem install cetusql
23
+ $ brew install fzf
24
+ $ brew install readline
25
+
26
+ ## Usage
27
+
28
+ run `cetusql` on the command line.
29
+
30
+ If no database name is passed, will prompt for one from *.db and *.sqlite files in current directory.
31
+ After selection, one may select one or more tables from a list.
32
+ Then one may select one ore more columns from a list.
33
+ Edit the SQL statement if need be, and press ENTER.
34
+ The output is displayed in vim using a temp file.
35
+
36
+ Use the menu to change table or database file, or set other options, save last SQL query, select from favorite queries, etc.
37
+
38
+ ## Development
39
+
40
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
41
+
42
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
43
+
44
+ ## Contributing
45
+
46
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rkumar/cetusql.
47
+
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
52
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cetusql"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/cetusql.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ #require 'cetusql/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cetusql"
8
+ spec.version = "0.0.1"
9
+ spec.authors = ["Rahul Kumar"]
10
+ spec.email = ["sentinel1879@gmail.com"]
11
+
12
+ spec.summary = %q{command line based sqlite db navigator}
13
+ spec.description = %q{command line based sqlite db navigator. ruby 1.9.3 .. 2.4.}
14
+ spec.homepage = "https://github.com/rkumar/cetusql"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ #spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ else
22
+ #raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ # http://bundler.io/blog/2015/03/20/moving-bins-to-exe.html
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.12"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_runtime_dependency 'sqlite3', '~> 1.3', '>= 1.3.11'
34
+ end
data/exe/cetussql ADDED
@@ -0,0 +1,975 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: cetusql
4
+ # Description: Fast database navigator for sqlite
5
+ # Author: rkumar
6
+ # Date: 2013-02-17 - 17:48
7
+ # License: MIT
8
+ # Last update: 2017-04-04 17:09
9
+ # ----------------------------------------------------------------------------- #
10
+ # cetussql Copyright (C) 2012-2017 rahul kumar
11
+ # == TODO
12
+ # - when in sql menu keep there, the menu of SQL is visible, it looks like we
13
+ # are still there.
14
+ # + toggle_menu - formatting, columns headers, no-history, no-indexinfo etc
15
+ # - try to get table name or query name in temp file name
16
+ # - if in db_menu key is unknown then try global menu and remove some options from other menus
17
+ # - reduce db_menu some options useless unless sql fired. put under SQL menu "s"
18
+ # - store history in another file, per database, not one file, getting too large.
19
+ # - the menus and hotkeys suck , need to be fixed.
20
+ # - define joins, so they can be reused
21
+ # + view_schema
22
+ # + make_query
23
+ # + menu keeps repeating don't show, can press ENTER or ` to view it
24
+ # -
25
+ # + order by is stored, so picked up against wrong database
26
+ # + if headers requested then we have to add them.
27
+ # last_sql must be for current database not just any. save under this database and retrieve under this database
28
+ # - up arrow on table menu should call edit_last_sql
29
+ # ? don't redraw the menu. the loop is outside the menu. keep it inside menu() so that it is not redrawn
30
+ # but exits upon q. what if user does not want that ?
31
+ #
32
+ # options : unformatted with tabs output - huge outputs take time
33
+ # STDOUT or file. specify output to a file
34
+ # header off and on
35
+ #
36
+ #
37
+ # == END TODO
38
+ require 'readline'
39
+ require 'io/wait' # may not need this
40
+ # http://www.ruby-doc.org/stdlib-1.9.3/libdoc/shellwords/rdoc/Shellwords.html
41
+ require 'shellwords'
42
+ require 'yaml'
43
+ #require 'fileutils'
44
+ # in ~/work/projects/common/
45
+ require 'cetusql/cli_utils'
46
+ require 'cetusql/cli_sqlite'
47
+ # -- requires 1.9.3 or higher for io/wait
48
+ # -- cannot do with Highline since we need a timeout on wait, not sure if HL can do that
49
+
50
+ VERSION="0.1.0"
51
+ O_CONFIG=true
52
+ CONFIG_FILE=File.expand_path("~/.cetusqlinfo")
53
+ # Using this to store data that has to be used in actions which do not take parameters or return values
54
+ # when called from a key
55
+ $options = {}
56
+ $options[:headers] = true
57
+ $options[:output_to] = nil
58
+ $options[:formatting] = true
59
+
60
+ $bindings = {}
61
+ $bindings[:db] = {
62
+ :t => :select_table,
63
+ :s => :sql_menu,
64
+ :m => :multi_select_table,
65
+ :l => :list_tables,
66
+ :q => :quit
67
+ }
68
+ $bindings[:table] = {
69
+ :v => :view_menu,
70
+ "4".to_sym => :select_columns,
71
+ "5".to_sym => :select_orderby,
72
+ "6".to_sym => :select_where,
73
+ "R".to_sym => :run_query,
74
+ "0".to_sym => :make_query,
75
+ :s => :save_sql,
76
+ :x => :new_query, # DDL no result enter a query and execute
77
+ :h => :history_menu, # queries for this table
78
+ :e => :edit_last_sql,
79
+ :O => :output_to,
80
+ :d => :change_database,
81
+ :j => :define_join,
82
+ "UP" => :edit_last_sql,
83
+ :q => :quit
84
+ }
85
+ $old_bindings = {
86
+ "`" => "main_menu",
87
+ "=" => "toggle_menu",
88
+ "!" => "command_mode",
89
+ "@" => "selection_mode_toggle",
90
+ "M-a" => "select_all",
91
+ "M-A" => "unselect_all",
92
+ "," => "goto_parent_dir",
93
+ "+" => "goto_dir",
94
+ "." => "pop_dir",
95
+ ":" => "subcommand",
96
+ "'" => "goto_bookmark",
97
+ "/" => "enter_regex",
98
+ "M-p" => "prev_page",
99
+ "M-n" => "next_page",
100
+ "SPACE" => "next_page",
101
+ "M-f" => "select_visited_files",
102
+ "M-d" => "select_used_dirs",
103
+ "M-b" => "select_bookmarks",
104
+ "M-m" => "create_bookmark",
105
+ "M-M" => "show_marks",
106
+ "C-c" => "escape",
107
+ "ESCAPE" => "escape",
108
+ "TAB" => "views",
109
+ "C-i" => "views",
110
+ "?" => "dirtree",
111
+ "ENTER" => "select_current",
112
+ "D" => "delete_file",
113
+ "M" => "file_actions most",
114
+ "Q" => "quit_command",
115
+ "RIGHT" => "column_next",
116
+ "LEFT" => "column_next 1",
117
+ "C-x" => "file_actions",
118
+ "M--" => "columns_incdec -1",
119
+ "M-+" => "columns_incdec 1",
120
+ "S" => "command_file list y ls -lh",
121
+ "L" => "command_file Page n less",
122
+ "C-d" => "cursor_scroll_dn",
123
+ "C-b" => "cursor_scroll_up",
124
+ "UP" => "cursor_up",
125
+ "DOWN" => "cursor_dn",
126
+ "C-SPACE" => "visual_mode_toggle",
127
+
128
+ "M-?" => "print_help",
129
+ "F1" => "print_help",
130
+ "F2" => "child_dirs",
131
+ "F3" => "dirtree",
132
+ "F4" => "tree",
133
+ "S-F1" => "dirtree",
134
+ "S-F2" => "tree"
135
+
136
+ }
137
+
138
+
139
+ $mode = nil
140
+ $glines=%x(tput lines).to_i
141
+ $gcols=%x(tput cols).to_i
142
+ $grows = $glines - 3
143
+ $pagesize = 60
144
+ $gviscols = 3
145
+ $pagesize = $grows * $gviscols
146
+ MSCROLL = 10
147
+ $quitting = false
148
+ $modified = $writing = false
149
+
150
+ #$help = "#{BOLD}M-?#{BOLD_OFF} Help #{BOLD}`#{BOLD_OFF} Menu #{BOLD}!#{BOLD_OFF} Command #{BOLD}=#{BOLD_OFF} Toggle #{BOLD}q#{BOLD_OFF} Quit "
151
+ $help = "#{BOLD}`#{BOLD_OFF} Menu #{BOLD}=#{BOLD_OFF} Toggle #{BOLD}q#{BOLD_OFF} Quit "
152
+
153
+ db = nil
154
+
155
+ ## main loop which calls all other programs
156
+ def run()
157
+ ctr=0
158
+ filename, db, tables = change_database ARGV[0]
159
+ #filename = ARGV[0] || getdbname
160
+ #if filename
161
+ #db = Database.new filename
162
+ #tables = db.tables
163
+ #end
164
+ exit unless db
165
+ $mode = :db
166
+ # TODO populate readline with earlier SQLS for this database
167
+ prompt = ""
168
+ ch, text = db_menu
169
+ process_number ch
170
+
171
+
172
+ while true
173
+ i = 0
174
+ # title
175
+ filename = $g_data[:filename]
176
+ db = $g_data[:db]
177
+ #print "#{GREEN}#{$help} #{BLUE}cetusql #{VERSION} :#{$mode}#{CLEAR}\n"
178
+ #t = "#{$title}"
179
+ #t = t[t.size-$gcols..-1] if t.size >= $gcols
180
+ #print "#{BOLD}#{t}#{CLEAR}\n"
181
+ # another query , change table, save sql, change db, select multiple tables
182
+ # schema, count, sample
183
+ # ZZZ XXX
184
+ #print "\r#{_mm}#{$patt} >"
185
+ ch = text = nil
186
+ #print "\r:#{$mode} >"
187
+ print "\r:%-5s >" % [$mode]
188
+ ch = get_char
189
+ print ch
190
+ # TODO
191
+ # depending on mode, we display menu if tilde pressed, or we process based on tilde
192
+ ch, text = process_key ch
193
+ puts ch if $opt_debug
194
+ if ch == "`" or ch == "ENTER"
195
+ if $mode == :table
196
+ display_menu "Menu for #{$mode} #{current_tablename}", $bindings[$mode]
197
+ else
198
+ display_menu "Menu for #{$mode} #{current_dbname}", $bindings[$mode]
199
+ end
200
+ elsif ch == '='
201
+ toggle_menu
202
+ end
203
+ if ch == "q" or ch == "ESCAPE" or ch == "C-c"
204
+ if $mode == :table
205
+ $mode = :db
206
+ $g_data[:current_tablename] = nil
207
+ else
208
+ break
209
+ end
210
+ end
211
+ #puts
212
+ #break if ch == "q" or ch == "ESCAPE" or ch == "C-c"
213
+ end
214
+ puts "bye"
215
+ $writing = true
216
+ config_write if $writing
217
+ end
218
+ def process_key ch
219
+ # get correct action map
220
+ h = $bindings[$mode] || $bindings[:db]
221
+ binding = h[ch]
222
+ binding = h[ch.to_sym] unless binding
223
+ if binding
224
+ if respond_to?(binding, true)
225
+ # 2017-03-19 - we can't send return values from a method ??
226
+ send(binding)
227
+ end
228
+ else
229
+ if $mode == :db
230
+ process_number ch
231
+ end
232
+ end
233
+ return ch, binding
234
+ end
235
+ def config_write
236
+ $g_data[:db] = nil
237
+ $g_data[:tables] = nil
238
+ writeYML $g_data, CONFIG_FILE
239
+ end
240
+ def config_read
241
+ if File.exist? CONFIG_FILE
242
+ $g_data = loadYML(CONFIG_FILE)
243
+ else
244
+ $g_data = {}
245
+ end
246
+ end
247
+
248
+ def loadYML( filename)
249
+ hash = YAML::load( File.open( filename ) )
250
+ #puts hash.keys.size
251
+ return hash
252
+ end
253
+
254
+ require 'fileutils' # for mkdir_p
255
+ def writeYML obj, filename
256
+ dir = File.dirname filename
257
+ unless File.exist? dir
258
+ FileUtils::mkdir_p dir
259
+ end
260
+ File.open(filename, 'w') {|f| f.write obj.to_yaml }
261
+ #$stderr.puts color("==> written to #{filename}", "green", "bold") unless $opt_quiet
262
+ $stderr.puts("==> written to #{filename}") unless $opt_quiet
263
+ end
264
+
265
+
266
+ def sql_menu
267
+ # check if there is an sql that has been fired, then save_sql and edit_last
268
+ # if table selected then select columns, order_by where etc
269
+ h = {
270
+ :s => :save_sql,
271
+ :h => :sql_history, # databases accessed, tables accessed, sqls issued
272
+ :e => :edit_last_sql,
273
+ :O => :output_to,
274
+ :F => :formatting_toggle,
275
+ :q => :quit
276
+ }
277
+ menu "SQL Menu for #{current_dbname}", h
278
+ end
279
+ def view_menu
280
+ h = {
281
+ :a => :view_all_rows,
282
+ :s => :view_sample,
283
+ :r => :view_recent,
284
+ :c => :view_schema,
285
+ :q => :quit
286
+ }
287
+ menu "View Menu for #{current_tablename}", h
288
+ end
289
+ def db_menu
290
+ h = {
291
+ :t => :select_table,
292
+ :m => :multi_select_table,
293
+ :l => :list_tables,
294
+ :s => :sql_menu,
295
+ #:d => :change_database,
296
+ "UP" => :edit_last_sql,
297
+ :q => :quit
298
+ }
299
+ menu "DB Menu for #{current_dbname}", h
300
+ end
301
+ def toggle_menu
302
+ h = { :f => :formatting_toggle, :h => :toggle_headers, :i => :toggle_index_info , "H" => :toggle_history,
303
+ :p => :toggle_pager_mode}
304
+ ch, menu_text = menu "Toggle Menu", h
305
+ case menu_text
306
+ when :toggle_headers
307
+ $options[:headers] = !$options[:headers]
308
+ pgreen "headers is #{$options[:headers]}"
309
+ when :toggle_index_info
310
+ # should index info be printed along with columsn, may take time if many tables
311
+ $options[:index_info] = !$options[:index_info]
312
+ pgreen "index_info calculation is #{$options[:index_info]}"
313
+ when :toggle_history
314
+ $options[:history_save] = !$options[:history_save]
315
+ pgreen "Saving SQL history is #{$options[:history_save]}"
316
+ when :toggle_pager_mode
317
+ $editor_mode = !$editor_mode
318
+ if $editor_mode
319
+ $default_command = nil
320
+ else
321
+ $default_command = ENV['MANPAGER'] || ENV['PAGER']
322
+ end
323
+ end
324
+ end
325
+ def process_number ch
326
+ # this is done in :db mode, to quick select a table, not in table mode since numbers are used
327
+ # if number is < 10 then see if there's a table and select that table
328
+ puts ch if $opt_debug
329
+ chi = ch.to_i
330
+ if chi > 0 and chi < 10
331
+ tables = fetch(:tables)
332
+ tablename = tables[chi - 1]
333
+ if tablename
334
+ pgreen "Selected #{tablename} "
335
+ select_table(tablename)
336
+ display_menu "Menu for #{tablename}", $bindings[$mode]
337
+ end
338
+ end
339
+ end
340
+ def table_menu tablename=nil
341
+ t = tablename || current_tablename()
342
+ h = {
343
+ "2".to_sym => :view_all_rows,
344
+ "3".to_sym => :view_sample,
345
+ "9".to_sym => :view_recent,
346
+ "4".to_sym => :select_columns,
347
+ "5".to_sym => :select_orderby,
348
+ "6".to_sym => :select_where,
349
+ "R".to_sym => :run_query,
350
+ "0".to_sym => :make_query,
351
+ :s => :save_sql,
352
+ :x => :new_query, # DDL no result enter a query and execute
353
+ :h => :history_menu, # queries for this table
354
+ :c => :view_schema,
355
+ :e => :edit_last_sql,
356
+ :O => :output_to,
357
+ :d => :change_database,
358
+ :j => :define_join,
359
+ "UP" => :edit_last_sql,
360
+ :q => :quit
361
+ }
362
+ menu "Table Menu for #{t}", h
363
+ end
364
+
365
+ # TODO
366
+ # 2017-03-27 - construct a query and run it.
367
+ # actually it should give a submenu
368
+ def make_query
369
+ # 1. select table if nil
370
+ # 2 select columns for display
371
+ # 3. select order
372
+ # 4. select where
373
+ # 5. select LIMIT
374
+ # 6. edit and run
375
+ tablename = current_tablename()
376
+ unless tablename
377
+ tablename = select_table
378
+ end
379
+ return unless tablename
380
+ puts "Select columns"
381
+ sel_columns = select_columns
382
+ puts "Select where condition"
383
+ where_str = select_where
384
+ puts "Select order by"
385
+ orderby_cols = select_orderby
386
+ run_query
387
+ end
388
+ def new_query
389
+ db = current_db()
390
+ tablename = current_tablename()
391
+ columns = db.columns(tablename)
392
+ #sql = "SELECT #{columns.join(",")} FROM #{tablename} ;"
393
+ sql = "SELECT\n#{columns.join(",\n")} \nFROM #{tablename} \nLIMIT 1000\n ;"
394
+ puts "created sql" if $opt_debug
395
+ edit_execute_in_editor sql
396
+ end
397
+ def edit_execute_in_editor sql
398
+ require 'tempfile'
399
+ tmpfile = Tempfile.new('SQL.XXXXXX')
400
+ filename = tmpfile.path
401
+ filename = Shellwords.escape(filename)
402
+ tmpfile.write(sql)
403
+ puts "written to #{filename}" if $opt_debug
404
+ tmpfile.close
405
+ mtime = File.mtime(filename)
406
+ puts mtime if $opt_debug
407
+ command = nil
408
+ #system "vim #{filename}" and (command = File.open(filename).readlines.join(" ") )
409
+ system "vim #{filename}"
410
+ mtime2 = File.mtime(filename)
411
+ puts mtime2 if $opt_debug
412
+ # we are comparing modification times of the file to see if user quit or saved.
413
+ # vim returns a zero in both cases, unless user quits using :cq
414
+ return if mtime2 == mtime
415
+ #puts "not returnig"
416
+ command = File.open(filename).readlines.join(" ")
417
+ if command
418
+ command = command.gsub("\n", " ")
419
+ #puts "got : #{command}" if command
420
+ puts "..."
421
+ view_sql command
422
+ end
423
+ set_last_sql command
424
+ end
425
+
426
+ def view_schema
427
+ tablename = current_tablename
428
+ puts
429
+ pbold "Schema for #{tablename}"
430
+ list_metadata tablename
431
+ indexes_for_table tablename
432
+ puts
433
+ end
434
+
435
+ def change_database filename=nil
436
+ # add to g_data as visited_dbs
437
+ filename = select_database_name unless filename
438
+ if filename
439
+ db = Database.new filename
440
+ tables = db.tables
441
+ $g_data[:filename] = filename
442
+ $g_data[:db] = db
443
+ $mode = :db
444
+ $g_data[:current_tablename] = nil
445
+
446
+ #$g_data[:tables] = tables
447
+ store(:tables, tables)
448
+ $g_data[:visited_dbs] ||= []
449
+ # TODO should be unique. remove if exists, then add
450
+ $g_data[:visited_dbs].delete(filename)
451
+ $g_data[:visited_dbs] << filename
452
+ $g_data[:last_sql] = nil
453
+ clear_screen
454
+ puts "#{BLUE}Using #{current_dbname}#{CLEAR}"
455
+ list_tables
456
+ list_indexes
457
+ end
458
+ return filename, db, tables
459
+ end
460
+ # get database names
461
+ def select_database_name
462
+ # Add recent ones, but remove directory portion if they belong to current dir
463
+ choices = Dir['*.db','*.sqlite','*.sqlite3'] | $g_data[:visited_dbs].map {|e| e.sub(Dir.pwd + "/","") }
464
+ if choices
465
+ if choices.size == 1
466
+ return File.expand_path(choices.first)
467
+ else
468
+ # select_from is in cli_utils.rb
469
+ #db = select_from "Select database", choices
470
+ #db = ctrlp choices
471
+ db = choose choices
472
+ return File.expand_path(db) if db
473
+ end
474
+ end
475
+ return nil
476
+ end
477
+ #alias :select_database_name :getdbname
478
+ def multi_select_table
479
+ db = current_db
480
+ sel_tables = multi_select "select tables ", db.tables
481
+ columns = []
482
+ sel_tables.each do |tablename|
483
+ c = db.columns tablename
484
+ c.each {|e| columns << "#{tablename}.#{e}" }
485
+ end
486
+ sel_columns = multi_select "select columns ", columns
487
+ #puts sel_columns.size
488
+ #puts sel_columns.class
489
+ sel_columns ||= ['*']
490
+ sql = "SELECT #{sel_columns.join(",")} FROM #{sel_tables.join(',')} \nWHERE\n LIMIT 1000\n ;"
491
+ puts sql
492
+ edit_execute_sql sql
493
+ # TODO edit it and view result
494
+ # TODO join based on commond fields, we have that code somewhere
495
+
496
+ end
497
+ def define_join
498
+ db = current_db
499
+ tablename = current_tablename()
500
+
501
+ sel_table = single_select "select lookup table for join:", db.tables
502
+ sel_table = sel_table.chomp
503
+ unless sel_table
504
+ return
505
+ end
506
+ columns = db.columns sel_table
507
+ sel_columns = multi_select "select columns to join ", columns
508
+ sel_columns_this = multi_select "select columns from this table ", db.columns( tablename)
509
+
510
+ str = tablename + "." + sel_columns_this.first + " = " + sel_table + "." + sel_columns.first
511
+ puts str
512
+ ca = sel_columns_this.first
513
+ cb = sel_columns.first
514
+ hash_put([:joins, tablename, sel_table, ca, cb])
515
+
516
+ #puts sel_columns.size
517
+ #puts sel_columns.class
518
+ # TODO edit it and view result
519
+ # TODO join based on commond fields, we have that code somewhere
520
+
521
+ end
522
+ # for current database create hash entries from a heirarchy, like mkdir -p
523
+ def hash_put array
524
+ h = $g_data[:databases][current_dbname()]
525
+ array.each do |e|
526
+ h[e] ||= {}
527
+ h = h[e]
528
+ end
529
+ end
530
+
531
+ # select a table and then puts user into table_menu (that is bad!)
532
+ def select_table tablename=nil
533
+ db = $g_data[:db]
534
+ filename = $g_data[:filename]
535
+ unless tablename
536
+ tablename = select_from "Select table from:(#{filename})", db.tables
537
+ end
538
+ unless tablename
539
+ if confirm("Do you wish to quit ? ")
540
+ $quitting = true
541
+ return false
542
+ end
543
+ return
544
+ end
545
+ clear_screen
546
+
547
+ $g_data[:current_tablename] = tablename
548
+ list_metadata tablename
549
+ indexes_for_table tablename
550
+ joins_for_table tablename
551
+ $mode = :table
552
+ return
553
+ end
554
+ # store some values against the current database
555
+ def store key, value
556
+ $g_data[:databases][current_dbname()] ||= {}
557
+ $g_data[:databases][current_dbname()][key] = value
558
+ #$g_data[key] = value
559
+ end
560
+ # retrieve some values against the current database
561
+ def fetch key
562
+ $g_data[:databases][current_dbname()] ||= {}
563
+ $g_data[:databases][current_dbname()][key]
564
+ #$g_data[key]
565
+ end
566
+ def select_columns
567
+ #puts "inside select_columns"
568
+ db = current_db()
569
+ tablename = current_tablename || return
570
+ columns = db.columns tablename
571
+ # TODO
572
+ # display columns with datatypes and indexes on them
573
+ # count of rows
574
+ # options for sample, first 100 rows last 100 rows (based on count)
575
+ # option for selecting column, where, order, RUN, all_cols,
576
+ sel_columns = multi_select "select column from #{tablename}", columns
577
+ #puts sel_columns.size
578
+ #puts sel_columns.class
579
+ #p sel_columns
580
+ sel_columns = ['*'] if sel_columns.size == 0
581
+ # FIXME these should be stored against a specific database and table, otherwise they will cause
582
+ # errors
583
+ store ':selected_columns', sel_columns
584
+ puts fetch(':selected_columns')
585
+ return sel_columns
586
+ end
587
+ def select_orderby
588
+ db = current_db()
589
+ tablename = current_tablename || exit(11)
590
+ columns = db.columns tablename
591
+ sel_columns = multi_select "select order by from #{tablename}", columns
592
+ sel_columns = nil if sel_columns.size == 0
593
+ store ':order_by', sel_columns
594
+ puts fetch(':order_by')
595
+ return sel_columns
596
+ end
597
+ def select_where
598
+ db = current_db()
599
+ tablename = current_tablename || exit(1)
600
+ columns = db.columns tablename
601
+ sel_columns = multi_select "select WHERE from #{tablename}", columns
602
+ sel_columns = nil if sel_columns.size == 0
603
+ res = nil
604
+ if sel_columns
605
+ a = []
606
+ sel_columns.each do |e|
607
+ s = input "WHERE #{e} "
608
+ a << "#{e} #{s}"
609
+ end
610
+ res = a.join(" AND ")
611
+ end
612
+ store ':where', res
613
+ puts fetch(':where')
614
+ return res
615
+ end
616
+ def run_query
617
+ t = current_tablename
618
+ c = fetch(':selected_columns') || ['*']
619
+ o = fetch(':order_by')
620
+ w = fetch(':where')
621
+ sql = "SELECT #{c.join(', ')} FROM #{t} "
622
+ if w
623
+ #sql += " WHERE #{w.join(', ')} "
624
+ sql += " WHERE #{w} "
625
+ end
626
+ if o
627
+ sql += " ORDER BY #{o.join(', ')} "
628
+ end
629
+ edit_execute_in_editor sql
630
+ #sql = vared sql, "Edit SQL: "
631
+ #return if sql.nil? or sql.size == 0
632
+ #set_last_sql sql
633
+ #view_sql sql
634
+ end
635
+ def edit_execute_sql sql
636
+ command = vared sql, "Edit SQL:"
637
+ if command.nil? or command.size == 0
638
+ return false
639
+ end
640
+ puts "..."
641
+ view_sql command
642
+ set_last_sql command
643
+ return true
644
+ end
645
+ def list_tables
646
+ db = current_db()
647
+ tables = current_db.tables
648
+ puts
649
+ data = [["#", "Table", "Rows", "Indexed"]]
650
+ ctr = 1
651
+ tables.each do |t|
652
+ #columns, datatypes = db.get_metadata t
653
+ content = db.get_data "SELECT count(1) from #{t} "
654
+ rows = content.first.first
655
+ indexed = '?'
656
+ if $options[:index_info]
657
+ indexed = db.indexed_columns(t) || "---"
658
+ end
659
+ char = ctr < 10 ? ctr.to_s : ""
660
+ row = [char, t, rows, indexed]
661
+ data << row
662
+ ctr += 1
663
+ #puts "#{t} #{rows} "
664
+ end
665
+ view_array data
666
+ end
667
+ # I don't need to loop through tables SILLY !
668
+ # FIXME just loop without tables
669
+ def list_indexes
670
+ db = current_db()
671
+ data = [["Index", "Table", "Column/s"]]
672
+ #columns, datatypes = db.get_metadata t
673
+ sql = %Q{SELECT name, tbl_name, sql FROM sqlite_master WHERE type = "index" ORDER BY tbl_name }
674
+ content = db.get_data sql
675
+ return if content.nil? or content == []
676
+ content.each do |r|
677
+ if r[-1] != nil
678
+ m = r[-1].match /\((.*)\)/
679
+ r[-1] = m[1] if m
680
+ end
681
+ row = [ *r ]
682
+ data << row
683
+ end
684
+ view_array data
685
+ end
686
+ def indexes_for_table table
687
+ db = current_db()
688
+ data = [["Index", "Table", "Column/s"]]
689
+ sql = %Q{SELECT name, tbl_name, sql FROM sqlite_master WHERE type = "index" and tbl_name = "#{table}" }
690
+ content = db.get_data sql
691
+ if content.nil? or content == []
692
+ pred "No indexes for table #{table}"
693
+ return
694
+ end
695
+ title = "Indexes of table: #{table}"
696
+ puts "-"*title.size
697
+ pbold title
698
+ puts "-"*title.size
699
+ content.each do |r|
700
+ if r[-1] != nil
701
+ m = r[-1].match /\((.*)\)/
702
+ r[-1] = m[1] if m
703
+ end
704
+ row = [ *r ]
705
+ data << row
706
+ end
707
+ view_array data
708
+ end
709
+ def joins_for_table tablename
710
+ arrow = "->"
711
+ checkmark = "\u2523"
712
+ checkmark = "\u279C"
713
+ db = current_db()
714
+ h = fetch(:joins)
715
+ return unless h
716
+ hj = h[tablename]
717
+ return unless hj
718
+ #p hj
719
+ pbold "Joins for table: #{tablename}"
720
+ hj.each_pair do |k, v|
721
+ puts " #{tablename} -> #{k} "
722
+ v.each_pair do |k1, v1|
723
+ puts " #{checkmark} #{tablename}.#{k1} = #{k}.#{v1.keys.first}"
724
+ end
725
+ end
726
+ end
727
+ # if too long split and columnate at 10 rows
728
+ def list_metadata table
729
+ db = current_db()
730
+ columns, datatypes = db.get_metadata table
731
+ array = []
732
+ title = "Columns of table: #{table}"
733
+ puts "-"*title.size
734
+ pbold title
735
+ puts "-"*title.size
736
+
737
+ columns.each_with_index do |e, ix|
738
+ #print "%-20s %s\n" % [e, datatypes[ix] ]
739
+ array << " %-20s #{GREEN}%-8s#{CLEAR} " % [e, datatypes[ix] ]
740
+ end
741
+ if false
742
+ array = columnate array, $grows - 7
743
+ array.each {|line| print line, "\n" }
744
+ end
745
+ print_in_cols array
746
+ #array.to_table
747
+
748
+ end
749
+ def config
750
+ $g_data[:databases][current_dbname()]
751
+ end
752
+ def sql_history
753
+ dbc = config()
754
+ hist = dbc[:history]
755
+ saved = dbc[:saved_sqls]
756
+ ctr = 1
757
+ all = []
758
+ if saved
759
+ pbold "Saved sqls"
760
+ saved.each_with_index {|e,i| puts "#{ctr} #{e}"; ctr += 1; }
761
+ all += saved
762
+ end
763
+ if hist
764
+ pbold "History"
765
+ hist.each_with_index {|e,i| puts "#{ctr} #{e}"; ctr += 1; }
766
+ all += hist
767
+ end
768
+ print "Select an sql: "
769
+ choice = $stdin.gets.chomp
770
+ puts choice
771
+ return if choice == ""
772
+ sql = all[choice.to_i - 1]
773
+ return unless sql
774
+ sql = vared sql, "Edit sql: "
775
+ return if sql.nil? or sql.size == 0
776
+ puts sql
777
+ view_sql sql
778
+ end
779
+
780
+ # choose from an array. printed vertically no columns, choose number
781
+ def choose array, title=nil, prompt=': '
782
+
783
+ pbold title if title
784
+ array.each_with_index {|e,i| puts "#{i+1} #{e}" }
785
+ while true
786
+ print "> \r (Enter 1 to #{array.size}) "
787
+ choice = $stdin.gets.chomp
788
+ if choice == "" or choice == "q"
789
+ return nil
790
+ end
791
+ chi = choice.to_i
792
+ if chi < 1 or chi > array.size
793
+ next
794
+ end
795
+ break
796
+ end
797
+ #puts choice
798
+ return nil if choice == ""
799
+ k = array[choice.to_i - 1]
800
+ return k
801
+ end
802
+ CSI = "\e["
803
+ def OLDchoose array, title=nil, prompt=': '
804
+
805
+ # why is save and resutore not working heee
806
+ $stdout.write "#{CSI}s" # save cursor position
807
+ while true
808
+ pbold title if title
809
+ array.each_with_index {|e,i| puts "#{i+1} #{e}" }
810
+ print prompt
811
+ choice = $stdin.gets.chomp
812
+ chi = choice.to_i
813
+ puts chi
814
+ if choice == "" or choice == "q"
815
+ return nil
816
+ end
817
+ if chi < 1 or chi > array.size
818
+ puts "restore"
819
+ $stdout.write "#{CSI}u" # restore cursor position
820
+ next
821
+ end
822
+ break
823
+ end
824
+ #puts choice
825
+ return nil if choice == ""
826
+ k = array[choice.to_i - 1]
827
+ return k
828
+ end
829
+
830
+ def view_sql sql
831
+ begin
832
+ view_data current_db, sql, $options
833
+ rescue => e
834
+ puts e.to_s
835
+ puts e.message
836
+ #puts e.backtrace.join("\n")
837
+ end
838
+
839
+ end
840
+
841
+
842
+ # send output of following commands to this file
843
+ def output_to filename=nil
844
+ unless filename
845
+ filename = input "Enter output filename: "
846
+ filename = filename.chomp
847
+ end
848
+ filename = nil if filename == ""
849
+ $options[:output_to] = filename
850
+ return filename
851
+ end
852
+ def formatting_toggle
853
+ $options[:formatting] = !$options[:formatting]
854
+ puts "Column formatting is now #{$options[:formatting]} "
855
+ pause
856
+ end
857
+ def current_db
858
+ $g_data[:db]
859
+ end
860
+ def current_dbname
861
+ $g_data[:filename] || "WARNING NOT SET: :filename"
862
+ end
863
+ def current_tablename
864
+ $g_data[:current_tablename]
865
+ end
866
+ # save an sql statement like a bookmark
867
+ # Would have liked to have a nickname for it, or title.
868
+ def save_sql sql=nil
869
+ sql ||= $g_data[:last_sql]
870
+ $g_data[:databases] ||= {}
871
+ $g_data[:databases][current_dbname()] ||= {}
872
+ $g_data[:databases][current_dbname()][:saved_sqls] ||= []
873
+ $g_data[:databases][current_dbname()][:saved_sqls].delete sql
874
+ $g_data[:databases][current_dbname()][:saved_sqls] << sql
875
+ $g_data[:databases][current_dbname()][:last_sql] = sql
876
+ pgreen "Saved sql statement"
877
+ $g_data[:last_sql] = nil
878
+ end
879
+ def set_last_sql sql
880
+ $g_data[:last_sql] = sql
881
+ # set history of sqls issued
882
+ $g_data[:databases] ||= {}
883
+ $g_data[:databases][current_dbname()] ||= {}
884
+ $g_data[:databases][current_dbname()][:last_sql] = sql
885
+ $g_data[:databases][current_dbname()][:history] ||= []
886
+ $g_data[:databases][current_dbname()][:history].delete sql
887
+ $g_data[:databases][current_dbname()][:history] << sql
888
+ end
889
+ def edit_last_sql
890
+ # this can be buggy, if you change database
891
+ #sql = $g_data[:last_sql]
892
+ sql = $g_data[:databases][current_dbname()][:last_sql]
893
+ #edit_execute_sql sql
894
+ edit_execute_in_editor sql
895
+ end
896
+ def view_array data
897
+ filename = tabulate2 data, $options
898
+ puts "Got #{filename} " if $opt_verbose
899
+ File.open(filename).readlines.each {|line| puts line}
900
+ end
901
+ # display all rows of current table
902
+ def view_all_rows
903
+ table = current_tablename()
904
+ #set_last_sql sql
905
+ sql = "SELECT * from #{table}"
906
+ view_sql sql
907
+ end
908
+ def view_sample
909
+ table = current_tablename()
910
+ #set_last_sql sql
911
+ sql = "SELECT * from #{table} LIMIT 100"
912
+ view_sql sql
913
+ end
914
+ # view last 100 inserted rows
915
+ def view_recent
916
+ table = current_tablename()
917
+ sql = current_db().sql_recent_rows(table)
918
+ view_sql sql
919
+ end
920
+ # uses fzf to select an old query
921
+ # fzf messes with quotes, so don't use
922
+ def history_menu
923
+
924
+ dbc = config()
925
+ array = []
926
+ hist = dbc[:history] || []
927
+ saved = dbc[:saved_sqls] || []
928
+ array = saved | hist
929
+ unless array
930
+ perror "No history for this database!"
931
+ return false
932
+ end
933
+ #sql = single_select "Select query: ", array
934
+ puts "Use up arrow to navigate queries, ^r to search:"
935
+ command = editline array
936
+ #command = vared sql, "Edit SQL:"
937
+ if command.nil? or command.size == 0
938
+ $quitting = true
939
+ return false
940
+ end
941
+ view_sql command
942
+ set_last_sql command
943
+ end
944
+ def display_menu title, h
945
+ puts "inside display_menu with #{title}, mode = #{$mode} " if $opt_debug
946
+ unless h
947
+ h = $bindings[:db]
948
+ end
949
+ return unless h
950
+ pbold "#{title}"
951
+ ctr = 0
952
+ #h.each_pair { |k, v| puts " #{k}: #{v}" }
953
+ h.each_pair { |k, v|
954
+ print " #{k}: %-20s" % [v]
955
+ if ctr > 1
956
+ print "\n"
957
+ ctr = 0
958
+ else
959
+ ctr += 1
960
+ end
961
+ }
962
+ print "\n"
963
+ end
964
+ def clear_screen
965
+ system "clear"
966
+ t = "#{GREEN}#{$help} #{BLUE}cetusql #{VERSION}#{CLEAR}"
967
+ print "#{BOLD}#{t}#{CLEAR}\n"
968
+ end
969
+
970
+
971
+
972
+ config_read
973
+ $g_data ||= {}
974
+
975
+ run