db-gui 0.0.4 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8036df4dccc81646a039ae1f596d9337146eb11d8c797db665baec7720af497
4
- data.tar.gz: d3b32fe1f26b471a278c0c120f1bd4ddf1e48d8d3c914454527df3814eb34ad5
3
+ metadata.gz: 32291b6eecdc2752f695361d340e0f00c6e32bfb9b9d57821bbfafe884a6adbf
4
+ data.tar.gz: c0e66b0a2362651790810c50166c27a75e44fe1da68df13c10019a43f3db22f0
5
5
  SHA512:
6
- metadata.gz: d74eb9ca5bcd71c75e0588f5f94f80277402066ebf26a2e49619c1f22fcc831c76fdf7c1f8707ea776ee9b7f88aee94550b3aa197d58823cc431c1bfbdb2e66b
7
- data.tar.gz: f1154b733b50e78dcb388ffdfc3415562ba07a1541cc40e527560ac1e44f3d2c1b9ffabad22484acdf3fa18f8486bb9ff0f526c80d8d0eb6303f8adb17e88a98
6
+ metadata.gz: 99f98299b9d09171625237220c2c0d2d687bcfbd703c3e6c9e46fa5bc67308b68bfa5d9637fac2e5940b0e05c83ddfe0a52ca2156dcbfb03e41203d775ca506e
7
+ data.tar.gz: 1d280fbf96dc8efd81054d36808218f3cd21c2eff8a63f2a6575dd815de6e05d94c6e388f80ec146b657c9e93f283f8f11bb9f19868b514f0160b6f9d9c21cb2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.1.1
4
+
5
+ - Automatically extend DB command timeout after timing out by retrying 6 times (7 times total) with exponential timeout increases
6
+ - Avoid DB command timeout if DB result row count is received
7
+ - Show error when DB command fails
8
+
9
+ ## 0.1.0
10
+
11
+ - Remember last DB command
12
+ - Move saved configuration from ~/.db_gui as a file to ~/.db_gui as a directory with multiple files underneath: ~/.db_gui/.db_configs & ~/.db_gui/.db_commands
13
+
3
14
  ## 0.0.4
4
15
 
5
16
  - Make SQL command entry a non_wrapping_multiline_entry
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
- # DB GUI (Database Graphical User Interface) 0.0.4
1
+ # DB GUI (Database Graphical User Interface) 0.1.1
2
2
  ## [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=40 /> Glimmer DSL for LibUI Application](https://github.com/AndyObtiva/glimmer-dsl-libui)
3
3
  [![Gem Version](https://badge.fury.io/rb/db-gui.svg)](http://badge.fury.io/rb/db-gui)
4
4
 
5
-
6
5
  This is an early alpha database graphical user interface that enables interaction with a relational SQL database.
7
6
 
8
7
  It currently supports PostgreSQL as a start, with the potential of supporting many other databases in the future.
@@ -13,7 +12,7 @@ It currently supports PostgreSQL as a start, with the potential of supporting ma
13
12
 
14
13
  Run:
15
14
  ```
16
- gem install db-gui -v0.0.4
15
+ gem install db-gui -v0.1.1
17
16
  ```
18
17
 
19
18
  ## Usage
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.1.1
@@ -0,0 +1,179 @@
1
+ require 'fileutils'
2
+ require 'timeout'
3
+
4
+ class DbGui
5
+ module Model
6
+ Db = Struct.new(:host, :port, :dbname, :username, :password, :db_command_timeout, keyword_init: true) do
7
+ DIR_DB_GUI = File.expand_path(File.join('~', '.db_gui'))
8
+ FileUtils.rm(DIR_DB_GUI) if File.file?(DIR_DB_GUI)
9
+ FileUtils.mkdir_p(DIR_DB_GUI)
10
+ FILE_DB_CONFIGS = File.expand_path(File.join(DIR_DB_GUI, '.db_configs'))
11
+ FILE_DB_COMMANDS = File.expand_path(File.join(DIR_DB_GUI, '.db_commands'))
12
+ COUNT_RETRIES = ENV.fetch('DB_COMMAND_COUNT_RETRIES', 7)
13
+ REGEX_ROW_COUNT = /^\((\d+) row/
14
+ ERROR_PREFIX = 'ERROR:'
15
+
16
+ attr_accessor :connected
17
+ alias connected? connected
18
+ attr_accessor :db_command_result
19
+ attr_accessor :db_command
20
+
21
+ def initialize
22
+ load_db_config
23
+ load_db_command
24
+ self.port ||= 5432 # PostgreSQL default port
25
+ self.db_command_result = ''
26
+ self.db_command_timeout ||= ENV.fetch('DB_COMMAND_TIMEOUT_IN_MILLISECONDS', 300).to_i
27
+ connect if to_h.except(:password).none? {|value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
28
+ end
29
+
30
+ def toggle_connection
31
+ if connected?
32
+ disconnect
33
+ else
34
+ connect
35
+ end
36
+ end
37
+
38
+ def connect
39
+ io
40
+ self.connected = true
41
+ save_db_config
42
+ end
43
+
44
+ def disconnect
45
+ io.close
46
+ @io = nil
47
+ self.connected = false
48
+ end
49
+
50
+ def io
51
+ db_connection_command = "PGPASSWORD=\"#{password}\" psql --host=#{host} --port=#{port} --username=#{username} --dbname=#{dbname}"
52
+ @io ||= IO.popen(db_connection_command, 'r+', err: [:child, :out])
53
+ end
54
+
55
+ def run_io_command(command)
56
+ command = command.strip
57
+ command = "#{command};" unless command.end_with?(';')
58
+ @io_command_try ||= 0
59
+ @io_command_try += 1
60
+ io.puts(command)
61
+ read_io_into_db_command_result
62
+ @io_command_try = nil
63
+ rescue Timeout::Error, Errno::EPIPE => e
64
+ puts e.message
65
+ @io = nil
66
+ if @io_command_try > COUNT_RETRIES
67
+ @io_command_try = nil
68
+ else
69
+ self.db_command_timeout *= 2
70
+ puts "Retrying DB Command... {try: #{@io_command_try + 1}, db_command_timeout: #{db_command_timeout}}"
71
+ run_io_command(command) unless db_command_result_error?
72
+ end
73
+ end
74
+
75
+ def run_db_command
76
+ run_io_command(db_command)
77
+ save_db_config
78
+ save_db_command
79
+ end
80
+
81
+ def db_command_result_count
82
+ db_command_result_count_headers_rows[0]
83
+ end
84
+
85
+ def db_command_result_headers
86
+ db_command_result_count_headers_rows[1]
87
+ end
88
+
89
+ def db_command_result_rows
90
+ db_command_result_count_headers_rows[2]
91
+ end
92
+
93
+ def db_command_result_error?
94
+ db_command_result.to_s.strip.start_with?(ERROR_PREFIX)
95
+ end
96
+
97
+ private
98
+
99
+ def read_io_into_db_command_result
100
+ self.db_command_result = read_db_command_result = ''
101
+ while (!(@line.to_s.match(REGEX_ROW_COUNT) || @line.to_s.strip == "^") && (@line = io_gets))
102
+ read_db_command_result += @line.to_s
103
+ end
104
+ self.db_command_result = read_db_command_result
105
+ rescue
106
+ if read_db_command_result.to_s.strip.start_with?(ERROR_PREFIX)
107
+ self.db_command_result = read_db_command_result
108
+ else
109
+ raise
110
+ end
111
+ ensure
112
+ @line = nil
113
+ end
114
+
115
+ def save_db_config
116
+ db_config_hash = to_h
117
+ db_configs_array = [db_config_hash] # TODO in the future, support storing multiple DB configs
118
+ db_configs_file_content = YAML.dump(db_configs_array)
119
+ File.write(FILE_DB_CONFIGS, db_configs_file_content)
120
+ end
121
+
122
+ def load_db_config
123
+ db_configs_file_content = File.read(FILE_DB_CONFIGS)
124
+ db_configs_array = [YAML.load(db_configs_file_content)].flatten
125
+ db_config_hash = db_configs_array.first # TODO in the future, support loading multiple DB configs
126
+ db_config_hash.each do |attribute, value|
127
+ self.send("#{attribute}=", value)
128
+ end
129
+ rescue => e
130
+ puts "No database configurations stored yet. #{e.message}"
131
+ end
132
+
133
+ def save_db_command
134
+ db_commands_array = [db_command] # TODO in the future, support storing multiple DB configs
135
+ db_commands_file_content = YAML.dump(db_commands_array)
136
+ File.write(FILE_DB_COMMANDS, db_commands_file_content)
137
+ end
138
+
139
+ def load_db_command
140
+ db_commands_file_content = File.read(FILE_DB_COMMANDS)
141
+ db_commands_array = YAML.load(db_commands_file_content)
142
+ self.db_command = db_commands_array.first
143
+ rescue => e
144
+ puts "No database commands stored yet. #{e.message}"
145
+ end
146
+
147
+ def io_gets
148
+ Timeout.timeout(db_command_timeout/1000.0) { io.gets }
149
+ rescue
150
+ @io = nil
151
+ raise
152
+ end
153
+
154
+ def db_command_result_count_headers_rows
155
+ if @db_command_result_count_headers_rows.nil? || db_command_result != @last_db_command_result
156
+ @db_command_result_count_headers_rows = compute_db_command_result_count_headers_rows
157
+ @last_db_command_result = db_command_result
158
+ end
159
+ @db_command_result_count_headers_rows
160
+ end
161
+
162
+ def compute_db_command_result_count_headers_rows
163
+ count = 0
164
+ headers = rows = []
165
+ db_command_result_lines = db_command_result.lines.reject { |line| line == "\n" }
166
+ if db_command_result_lines.any?
167
+ headers = db_command_result_lines.first.split('|').map(&:strip)
168
+ count_footer = db_command_result_lines.last
169
+ count_match = count_footer.match(REGEX_ROW_COUNT)
170
+ if count_match
171
+ count = count_match[1].to_i
172
+ rows = db_command_result_lines[2..-2].map {|row| row.split('|').map(&:strip) }
173
+ end
174
+ end
175
+ [count, headers, rows]
176
+ end
177
+ end
178
+ end
179
+ end
@@ -1,4 +1,4 @@
1
- require 'db_gui/model/db_config'
1
+ require 'db_gui/model/db'
2
2
 
3
3
  class DbGui
4
4
  module View
@@ -7,12 +7,12 @@ class DbGui
7
7
 
8
8
  TIMEOUT_MAX_IN_MILLISECONDS = (ENV['TIMEOUT_MAX_IN_MILLISECONDS'] || 60*60*1000).to_i
9
9
 
10
- option :db_config
10
+ option :db
11
11
 
12
12
  body {
13
13
  vertical_box {
14
14
  non_wrapping_multiline_entry {
15
- text <=> [db_config, :db_command]
15
+ text <=> [db, :db_command]
16
16
  }
17
17
 
18
18
  horizontal_box {
@@ -20,7 +20,7 @@ class DbGui
20
20
 
21
21
  button('Run') {
22
22
  on_clicked do
23
- db_config.run_db_command
23
+ db.run_db_command
24
24
  end
25
25
  }
26
26
 
@@ -29,7 +29,7 @@ class DbGui
29
29
  }
30
30
  spinbox(0, TIMEOUT_MAX_IN_MILLISECONDS) {
31
31
  stretchy false
32
- value <=> [db_config, :db_command_timeout]
32
+ value <=> [db, :db_command_timeout, on_read: :to_i]
33
33
  }
34
34
 
35
35
  label('Row(s): ') {
@@ -37,7 +37,7 @@ class DbGui
37
37
  }
38
38
  label {
39
39
  stretchy false
40
- text <= [db_config, :db_command_result_count, computed_by: :db_command_result, on_read: :to_s]
40
+ text <= [db, :db_command_result_count, computed_by: :db_command_result, on_read: :to_s]
41
41
  }
42
42
  }
43
43
  }
@@ -1,22 +1,24 @@
1
- require 'db_gui/model/db_config'
1
+ require 'db_gui/model/db'
2
2
 
3
3
  class DbGui
4
4
  module View
5
5
  class DbCommandResultTable
6
6
  include Glimmer::LibUI::CustomControl
7
7
 
8
- option :db_config
8
+ option :db
9
9
 
10
10
  body {
11
11
  vertical_box {
12
- content(db_config, :db_command_result) {
13
- if db_config.db_command_result_count > 0
12
+ content(db, :db_command_result) {
13
+ if db.db_command_result_error?
14
+ label(db.db_command_result)
15
+ elsif db.db_command_result_count > 0
14
16
  table {
15
- db_config.db_command_result_headers.each do |header|
17
+ db.db_command_result_headers.each do |header|
16
18
  text_column(header)
17
19
  end
18
20
 
19
- cell_rows db_config.db_command_result_rows
21
+ cell_rows db.db_command_result_rows
20
22
  }
21
23
  else
22
24
  label('No data')
@@ -1,52 +1,52 @@
1
- require 'db_gui/model/db_config'
1
+ require 'db_gui/model/db'
2
2
 
3
3
  class DbGui
4
4
  module View
5
5
  class DbConfigForm
6
6
  include Glimmer::LibUI::CustomControl
7
7
 
8
- option :db_config
8
+ option :db
9
9
 
10
10
  body {
11
11
  vertical_box {
12
12
  form {
13
13
  entry {
14
14
  label 'Host:'
15
- text <=> [db_config, :host]
16
- enabled <= [db_config, :connected, on_read: :!]
15
+ text <=> [db, :host]
16
+ enabled <= [db, :connected, on_read: :!]
17
17
  }
18
18
 
19
19
  spinbox(0, 1_000_000) {
20
20
  label 'Port:'
21
- value <=> [db_config, :port]
22
- enabled <= [db_config, :connected, on_read: :!]
21
+ value <=> [db, :port]
22
+ enabled <= [db, :connected, on_read: :!]
23
23
  }
24
24
 
25
25
  entry {
26
26
  label 'Database Name:'
27
- text <=> [db_config, :dbname]
28
- enabled <= [db_config, :connected, on_read: :!]
27
+ text <=> [db, :dbname]
28
+ enabled <= [db, :connected, on_read: :!]
29
29
  }
30
30
 
31
31
  entry {
32
32
  label 'Username:'
33
- text <=> [db_config, :username]
34
- enabled <= [db_config, :connected, on_read: :!]
33
+ text <=> [db, :username]
34
+ enabled <= [db, :connected, on_read: :!]
35
35
  }
36
36
 
37
37
  password_entry {
38
38
  label 'Password:'
39
- text <=> [db_config, :password]
40
- enabled <= [db_config, :connected, on_read: :!]
39
+ text <=> [db, :password]
40
+ enabled <= [db, :connected, on_read: :!]
41
41
  }
42
42
  }
43
43
 
44
44
  button {
45
45
  stretchy false
46
- text <= [db_config, :connected, on_read: -> (connected) { connected ? 'Disconnect (currently connected)' : 'Connect (currently disconnected)' }]
46
+ text <= [db, :connected, on_read: -> (connected) { connected ? 'Disconnect (currently connected)' : 'Connect (currently disconnected)' }]
47
47
 
48
48
  on_clicked do
49
- db_config.toggle_connection
49
+ db.toggle_connection
50
50
  end
51
51
  }
52
52
  }
@@ -1,4 +1,4 @@
1
- require 'db_gui/presenter/db_gui_presenter'
1
+ require 'db_gui/model/db'
2
2
 
3
3
  require 'db_gui/view/db_config_form'
4
4
  require 'db_gui/view/db_command_form'
@@ -9,10 +9,10 @@ class DbGui
9
9
  class DbGuiApplication
10
10
  include Glimmer::LibUI::Application
11
11
 
12
- attr_reader :presenter
12
+ attr_reader :db
13
13
 
14
14
  before_body do
15
- @presenter = Presenter::DbGuiPresenter.new
15
+ @db = Model::Db.new
16
16
  # menu_bar # TODO implement
17
17
  end
18
18
 
@@ -24,13 +24,13 @@ class DbGui
24
24
  margined true
25
25
 
26
26
  vertical_box {
27
- db_config_form(db_config: presenter.db_config) {
27
+ db_config_form(db:) {
28
28
  stretchy false
29
29
  }
30
30
 
31
- db_command_form(db_config: presenter.db_config)
31
+ db_command_form(db:)
32
32
 
33
- db_command_result_table(db_config: presenter.db_config)
33
+ db_command_result_table(db:)
34
34
  }
35
35
  }
36
36
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db-gui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-21 00:00:00.000000000 Z
11
+ date: 2025-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer-dsl-libui
@@ -86,8 +86,7 @@ files:
86
86
  - VERSION
87
87
  - app/db-gui.rb
88
88
  - app/db_gui/launch.rb
89
- - app/db_gui/model/db_config.rb
90
- - app/db_gui/presenter/db_gui_presenter.rb
89
+ - app/db_gui/model/db.rb
91
90
  - app/db_gui/view/db_command_form.rb
92
91
  - app/db_gui/view/db_command_result_table.rb
93
92
  - app/db_gui/view/db_config_form.rb
@@ -1,137 +0,0 @@
1
- require 'timeout'
2
-
3
- class DbGui
4
- module Model
5
- # TODO consider renaming to DB connection
6
- DbConfig = Struct.new(:host, :port, :dbname, :username, :password, keyword_init: true) do
7
- FILE_DB_CONFIG = File.expand_path(File.join('~', '.db_gui'))
8
-
9
- attr_accessor :connected
10
- alias connected? connected
11
- attr_accessor :db_command_result
12
- attr_accessor :db_command
13
- attr_accessor :db_command_timeout
14
-
15
- def initialize
16
- self.port = 5432 # PostgreSQL default port
17
- self.db_command_result = ''
18
- self.db_command_timeout = (ENV['DB_COMMAND_TIMEOUT_IN_MILLISECONDS'] || 300).to_i
19
- load_db_config
20
- connect if to_a.none? {|value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
21
- end
22
-
23
- def toggle_connection
24
- if connected?
25
- disconnect
26
- else
27
- connect
28
- end
29
- end
30
-
31
- def connect
32
- io
33
- self.connected = true
34
- save_db_config
35
- end
36
-
37
- def disconnect
38
- io.close
39
- @io = nil
40
- self.connected = false
41
- end
42
-
43
- def io
44
- @io ||= IO.popen("PGPASSWORD=\"#{password}\" psql --host=#{host} --port=#{port} --username=#{username} --dbname=#{dbname}", 'r+')
45
- end
46
-
47
- def run_io_command(command)
48
- command = command.strip
49
- command = "#{command};" unless command.end_with?(';')
50
- @io_command_try ||= 0
51
- @io_command_try += 1
52
- io.puts(command)
53
- read_io_into_db_command_result
54
- rescue Errno::EPIPE => e
55
- @io = nil
56
- run_io_command(command) unless @io_command_try > 1
57
- end
58
-
59
- def run_db_command
60
- run_io_command(db_command)
61
- end
62
-
63
- def db_command_result_count
64
- db_command_result_count_headers_rows[0]
65
- end
66
-
67
- def db_command_result_headers
68
- db_command_result_count_headers_rows[1]
69
- end
70
-
71
- def db_command_result_rows
72
- db_command_result_count_headers_rows[2]
73
- end
74
-
75
- private
76
-
77
- def read_io_into_db_command_result
78
- self.db_command_result = ''
79
- while (line = io_gets)
80
- result = line.to_s
81
- self.db_command_result += result
82
- end
83
- rescue Errno::EPIPE => e
84
- @io = nil
85
- end
86
-
87
- def save_db_config
88
- db_config_hash = to_h
89
- db_config_file_content = YAML.dump(db_config_hash)
90
- File.write(FILE_DB_CONFIG, db_config_file_content)
91
- end
92
-
93
- def load_db_config
94
- db_config_file_content = File.read(FILE_DB_CONFIG)
95
- db_config_hash = YAML.load(db_config_file_content)
96
- db_config_hash.each do |attribute, value|
97
- self.send("#{attribute}=", value)
98
- end
99
- rescue => e
100
- puts "No database configuration is stored yet. #{e.message}"
101
- end
102
-
103
- def io_gets
104
- # TODO figure out a way of knowing the end of input without timing out
105
- Timeout.timeout(db_command_timeout/1000.0) { io.gets }
106
- rescue
107
- @io = nil
108
- nil
109
- end
110
-
111
- def db_command_result_count_headers_rows
112
- if @db_command_result_count_headers_rows.nil? || db_command_result != @last_db_command_result
113
- @db_command_result_count_headers_rows = compute_db_command_result_count_headers_rows
114
- @last_db_command_result = db_command_result
115
- end
116
- @db_command_result_count_headers_rows
117
- end
118
-
119
- def compute_db_command_result_count_headers_rows
120
- count = 0
121
- headers = rows = []
122
- db_command_result_lines = db_command_result.lines
123
- db_command_result_lines.pop if db_command_result_lines.last == "\n"
124
- if db_command_result_lines.any?
125
- headers = db_command_result_lines.first.split('|').map(&:strip)
126
- count_footer = db_command_result_lines.last
127
- count_match = count_footer.match(/^\((\d+) row/)
128
- if count_match
129
- count = count_match[1].to_i
130
- rows = db_command_result_lines[2..-2].map {|row| row.split('|').map(&:strip) }
131
- end
132
- end
133
- [count, headers, rows]
134
- end
135
- end
136
- end
137
- end
@@ -1,13 +0,0 @@
1
- require 'db_gui/model/db_config'
2
-
3
- class DbGui
4
- module Presenter
5
- class DbGuiPresenter
6
- attr_reader :db_config
7
-
8
- def initialize
9
- @db_config = Model::DbConfig.new
10
- end
11
- end
12
- end
13
- end