sqlui 0.1.59 → 0.1.61

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: 6b06af3454b65600eacd42b1008e186c473736411fedaed471162af97d98f39f
4
- data.tar.gz: 38c46eb311a518ea328acd959ca34c8e5d850c83fa9c1b98c663daef2356c334
3
+ metadata.gz: ec12b1d88bc2adff9dba6ff91b01e66b123d17cb8b042e142c24f71303a27a92
4
+ data.tar.gz: df939b59b2bae004270ea1907a6be52352e7c81e6050405bc4534a6faaae0567
5
5
  SHA512:
6
- metadata.gz: 320e438a5101b83a59691168f2673f82ffb35559b547a9e75e6278b9735c4880a3906b298ae576d89346218f3ff3543e7cd57e034dbbfa2c525b4a069ad337ab
7
- data.tar.gz: 03d5d65cad39f143c56f205c1f55280d2ab269390191b0ea026300e1c422b896876c2e5cf0c9f0764ca712a50c9702fb222f93a584960bfe11762fbc4663a78f
6
+ metadata.gz: fd504e8ba6cf53b06b30b21867a7925ef7fda8055690eefb7759dd8ed745dd28088ca6c2f257eef1073aa976b67548c155bc26eb4f334750df6a059e9abdf6db
7
+ data.tar.gz: 96584bb97da39f508542a861d16d1bb2196238ab4d12c99528937704f6b29aefee9563c837921dbee1a72fe5113b3a8dfb39b4c74a5951ffc6fb8be697d535cb
data/.release-version CHANGED
@@ -1 +1 @@
1
- 0.1.59
1
+ 0.1.61
@@ -14,8 +14,8 @@ class DatabaseConfig
14
14
  @display_name = Args.fetch_non_empty_string(hash, :display_name).strip
15
15
  @description = Args.fetch_non_empty_string(hash, :description).strip
16
16
  @url_path = Args.fetch_non_empty_string(hash, :url_path).strip
17
- raise ArgumentError, 'url_path should start with a /' unless @url_path.start_with?('/')
18
- raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
17
+ raise ArgumentError, 'url_path should not start with a /' if @url_path.start_with?('/')
18
+ raise ArgumentError, 'url_path should not end with a /' if @url_path.end_with?('/')
19
19
 
20
20
  @saved_path = Args.fetch_non_empty_string(hash, :saved_path).strip
21
21
 
data/app/server.rb CHANGED
@@ -25,6 +25,9 @@ class Server < Sinatra::Base
25
25
  def self.init_and_run(config, resources_dir)
26
26
  logger.info("Starting SQLUI v#{Version::SQLUI}")
27
27
  logger.info("Airbrake enabled: #{config.airbrake[:server]&.[](:enabled) || false}")
28
+
29
+ WEBrick::HTTPRequest.const_set('MAX_URI_LENGTH', 2 * 1024 * 1024)
30
+
28
31
  if config.airbrake[:server]&.[](:enabled)
29
32
  require 'airbrake'
30
33
  require 'airbrake/rack'
@@ -59,51 +62,53 @@ class Server < Sinatra::Base
59
62
  set :raise_errors, false
60
63
  set :show_exceptions, false
61
64
 
62
- favicon_hash = Digest::MD5.hexdigest(File.read(File.join(resources_dir, 'favicon.svg')))
63
- css_hash = Digest::MD5.hexdigest(File.read(File.join(resources_dir, 'sqlui.css')))
64
- js_hash = Digest::MD5.hexdigest(File.read(File.join(resources_dir, 'sqlui.js')))
65
-
66
65
  get '/-/health' do
67
66
  status 200
68
67
  body 'OK'
69
68
  end
70
69
 
71
70
  get '/?' do
72
- redirect config.list_url_path, 301
71
+ redirect config.base_url_path, 301
73
72
  end
74
73
 
75
- get '/favicon.svg' do
76
- headers 'Cache-Control' => 'max-age=31536000'
77
- send_file File.join(resources_dir, 'favicon.svg')
74
+ resource_path_map = {}
75
+ Dir.glob(File.join(resources_dir, '*')).each do |file|
76
+ hash = Digest::MD5.hexdigest(File.read(file))
77
+ basename = File.basename(file)
78
+ url_path = "#{config.base_url_path}/#{basename}"
79
+ case File.extname(basename)
80
+ when '.svg'
81
+ content_type = 'image/svg+xml; charset=utf-8'
82
+ when '.css'
83
+ content_type = 'text/css; charset=utf-8'
84
+ when '.js'
85
+ content_type = 'text/javascript; charset=utf-8'
86
+ else
87
+ raise "unsupported resource file extension: #{File.extname(basename)}"
88
+ end
89
+ resource_path_map[basename] = "#{url_path}?#{hash}"
90
+ get url_path do
91
+ headers 'Content-Type' => content_type
92
+ headers 'Cache-Control' => 'max-age=31536000'
93
+ send_file file
94
+ end
78
95
  end
79
96
 
80
- get "#{config.list_url_path}/?" do
97
+ get "#{config.base_url_path}/?" do
81
98
  headers 'Cache-Control' => 'no-cache'
82
- erb :databases, locals: { config: config }
99
+ erb :databases, locals: { config: config, resource_path_map: resource_path_map }
83
100
  end
84
101
 
85
102
  config.database_configs.each do |database|
86
- get "#{database.url_path}/?" do
103
+ get "#{config.base_url_path}/#{database.url_path}/?" do
87
104
  redirect "#{database.url_path}/query", 301
88
105
  end
89
106
 
90
- get "#{database.url_path}/sqlui.css" do
91
- headers 'Content-Type' => 'text/css; charset=utf-8'
92
- headers 'Cache-Control' => 'max-age=31536000'
93
- send_file File.join(resources_dir, 'sqlui.css')
94
- end
95
-
96
- get "#{database.url_path}/sqlui.js" do
97
- headers 'Content-Type' => 'text/javascript; charset=utf-8'
98
- headers 'Cache-Control' => 'max-age=31536000'
99
- send_file File.join(resources_dir, 'sqlui.js')
100
- end
101
-
102
- post "#{database.url_path}/metadata" do
107
+ post "#{config.base_url_path}/#{database.url_path}/metadata" do
103
108
  metadata = database.with_client do |client|
104
109
  {
105
110
  server: "#{config.name} - #{database.display_name}",
106
- list_url_path: config.list_url_path,
111
+ base_url_path: config.base_url_path,
107
112
  schemas: DatabaseMetadata.lookup(client, database),
108
113
  tables: database.tables,
109
114
  columns: database.columns,
@@ -131,7 +136,7 @@ class Server < Sinatra::Base
131
136
  body metadata.to_json
132
137
  end
133
138
 
134
- post "#{database.url_path}/query" do
139
+ post "#{config.base_url_path}/#{database.url_path}/query" do
135
140
  data = request.body.read
136
141
  request.body.rewind # since Airbrake will read the body on error
137
142
  params.merge!(JSON.parse(data, symbolize_names: true))
@@ -143,31 +148,60 @@ class Server < Sinatra::Base
143
148
  status 200
144
149
  headers 'Content-Type' => 'application/json; charset=utf-8'
145
150
 
146
- database.with_client do |client|
147
- query_result = execute_query(client, variables, queries)
148
- stream do |out|
151
+ stream do |out|
152
+ database.with_client do |client|
153
+ begin
154
+ query_result = execute_query(client, variables, queries)
155
+ rescue Mysql2::Error => e
156
+ stacktrace = e.full_message(highlight: false)
157
+ message = "ERROR #{e.error_number} (#{e.sql_state}): #{e.message.lines.first&.strip || 'unknown error'}"
158
+ out << { error: message, stacktrace: stacktrace }.compact.to_json
159
+ break
160
+ rescue StandardError => e
161
+ stacktrace = e.full_message(highlight: false)
162
+ message = e.message.lines.first&.strip || 'unknown error'
163
+ out << { error: message, stacktrace: stacktrace }.compact.to_json
164
+ break
165
+ end
166
+
149
167
  if query_result
150
168
  json = <<~RES.chomp
151
169
  {
152
170
  "columns": #{query_result.fields.to_json},
153
171
  "column_types": #{MysqlTypes.map_to_google_charts_types(query_result.field_types).to_json},
154
- "total_rows": #{query_result.size.to_json},
155
172
  "selection": #{params[:selection].to_json},
156
173
  "query": #{params[:sql].to_json},
157
174
  "rows": [
158
175
  RES
159
176
  out << json
160
- bytes = json.bytesize
177
+ bytes_written = json.bytesize
178
+ max_rows_written = false
179
+ rows_written = 0
180
+ total_rows = 0
161
181
  query_result.each_with_index do |row, i|
182
+ total_rows += 1
183
+ next if max_rows_written
184
+
162
185
  json = "#{i.zero? ? '' : ','}\n #{row.map { |v| big_decimal_to_float(v) }.to_json}"
163
- bytes += json.bytesize
164
- break if i == Sqlui::MAX_ROWS || bytes > Sqlui::MAX_BYTES
186
+ bytesize = json.bytesize
187
+ if bytes_written + bytesize > Sqlui::MAX_BYTES
188
+ max_rows_written = true
189
+ next
190
+ end
165
191
 
166
192
  out << json
193
+ bytes_written += bytesize
194
+ rows_written += 1
195
+
196
+ if rows_written == Sqlui::MAX_ROWS
197
+ max_rows_written = true
198
+ next
199
+ end
167
200
  end
168
201
  out << <<~RES
169
202
 
170
- ]
203
+ ],
204
+ "total_rows": #{total_rows}
171
205
  }
172
206
  RES
173
207
  else
@@ -186,7 +220,7 @@ class Server < Sinatra::Base
186
220
  end
187
221
  end
188
222
 
189
- get "#{database.url_path}/download_csv" do
223
+ get "#{config.base_url_path}/#{database.url_path}/download_csv" do
190
224
  break client_error('missing sql') unless params[:sql]
191
225
 
192
226
  sql = Base64.decode64(params[:sql]).force_encoding('UTF-8')
@@ -198,9 +232,21 @@ class Server < Sinatra::Base
198
232
  attachment 'result.csv'
199
233
  status 200
200
234
 
201
- database.with_client do |client|
202
- query_result = execute_query(client, variables, queries)
203
- stream do |out|
235
+ stream do |out|
236
+ database.with_client do |client|
237
+ begin
238
+ query_result = execute_query(client, variables, queries)
239
+ rescue Mysql2::Error => e
240
+ stacktrace = e.full_message(highlight: false)
241
+ message = "ERROR #{e.error_number} (#{e.sql_state}): #{e.message.lines.first&.strip || 'unknown error'}"
242
+ out << { error: message, stacktrace: stacktrace }.compact.to_json
243
+ break
244
+ rescue StandardError => e
245
+ stacktrace = e.full_message(highlight: false)
246
+ message = e.message.lines.first&.strip || 'unknown error'
247
+ out << { error: message, stacktrace: stacktrace }.compact.to_json
248
+ break
249
+ end
204
250
  out << CSV::Row.new(query_result.fields, query_result.fields, header_row: true).to_s.strip
205
251
  query_result.each do |row|
206
252
  out << "\n#{CSV::Row.new(query_result.fields, row.map { |v| big_decimal_to_float(v) }).to_s.strip}"
@@ -209,7 +255,7 @@ class Server < Sinatra::Base
209
255
  end
210
256
  end
211
257
 
212
- get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
258
+ get(/#{Regexp.escape("#{config.base_url_path}/#{database.url_path}/")}(query|graph|structure|saved)/) do
213
259
  status 200
214
260
  headers 'Cache-Control' => 'no-cache'
215
261
  client_config = config.airbrake[:client] || {}
@@ -218,9 +264,7 @@ class Server < Sinatra::Base
218
264
  airbrake_enabled: client_config[:enabled] || false,
219
265
  airbrake_project_id: client_config[:project_id] || '',
220
266
  airbrake_project_key: client_config[:project_key] || '',
221
- js_hash: js_hash,
222
- css_hash: css_hash,
223
- favicon_hash: favicon_hash
267
+ resource_path_map: resource_path_map
224
268
  }
225
269
  end
226
270
  end
@@ -230,12 +274,17 @@ class Server < Sinatra::Base
230
274
  stacktrace = exception&.full_message(highlight: false)
231
275
  if request.env['HTTP_ACCEPT'] == 'application/json'
232
276
  headers 'Content-Type' => 'application/json; charset=utf-8'
233
- message = exception&.message&.lines&.first&.strip || 'unexpected error'
277
+ message = "error: #{exception&.message&.lines&.first&.strip || 'unexpected error'}"
234
278
  json = { error: message, stacktrace: stacktrace }.compact.to_json
235
279
  body json
236
280
  else
237
281
  message = "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
238
- erb :error, locals: { title: "SQLUI #{message}", message: message, stacktrace: stacktrace }
282
+ erb :error, locals: {
283
+ resource_path_map: resource_path_map,
284
+ title: "SQLUI #{message}",
285
+ message: message,
286
+ stacktrace: stacktrace
287
+ }
239
288
  end
240
289
  end
241
290
 
@@ -274,7 +323,10 @@ class Server < Sinatra::Base
274
323
  variables.each do |name, value|
275
324
  client.query("SET @#{name} = #{value};")
276
325
  end
277
- queries.map { |current| client.query(current) }.last
326
+ queries[0..-2].map do |current|
327
+ client.query(current, stream: true)&.free
328
+ end
329
+ client.query(queries[-1], stream: true)
278
330
  end
279
331
 
280
332
  def big_decimal_to_float(maybe_big_decimal)
data/app/sqlui_config.rb CHANGED
@@ -8,7 +8,7 @@ require_relative 'deep'
8
8
 
9
9
  # App config including database configs.
10
10
  class SqluiConfig
11
- attr_reader :name, :port, :environment, :list_url_path, :database_configs, :airbrake
11
+ attr_reader :name, :port, :environment, :base_url_path, :database_configs, :airbrake
12
12
 
13
13
  def initialize(filename, overrides = {})
14
14
  config = YAML.safe_load(ERB.new(File.read(filename)).result, aliases: true).deep_merge!(overrides)
@@ -19,10 +19,10 @@ class SqluiConfig
19
19
  @name = Args.fetch_non_empty_string(config, :name).strip
20
20
  @port = Args.fetch_non_empty_int(config, :port)
21
21
  @environment = Args.fetch_non_empty_string(config, :environment).strip
22
- @list_url_path = Args.fetch_non_empty_string(config, :list_url_path).strip
23
- raise ArgumentError, 'list_url_path should start with a /' unless @list_url_path.start_with?('/')
24
- if @list_url_path.length > 1 && @list_url_path.end_with?('/')
25
- raise ArgumentError, 'list_url_path should not end with a /'
22
+ @base_url_path = Args.fetch_non_empty_string(config, :base_url_path).strip
23
+ raise ArgumentError, 'base_url_path should start with a /' unless @base_url_path.start_with?('/')
24
+ if @base_url_path.length > 1 && @base_url_path.end_with?('/')
25
+ raise ArgumentError, 'base_url_path should not end with a /'
26
26
  end
27
27
 
28
28
  databases = Args.fetch_non_empty_hash(config, :databases)
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <title>SQLUI <%= config.name %> Databases</title>
6
- <link rel="icon" type="image/x-icon" href="/favicon.svg">
6
+ <link rel="icon" type="image/x-icon" href="<%= resource_path_map['favicon.svg'] %>">
7
7
 
8
8
  <style>
9
9
  body {
@@ -92,9 +92,9 @@
92
92
  <div class="database">
93
93
  <div class="name-and-links">
94
94
  <h2 class='name'><%= database_config.display_name %></h2>
95
- <a class='query-link' href="<%= database_config.url_path %>/query">query</a>
96
- <a class='saved-link' href="<%= database_config.url_path %>/saved">saved</a>
97
- <a class='structure-link' href="<%= database_config.url_path %>/structure">structure</a>
95
+ <a class='query-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/query" %>">query</a>
96
+ <a class='saved-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/saved" %>">saved</a>
97
+ <a class='structure-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/structure" %>">structure</a>
98
98
  </div>
99
99
  <p class='description'>
100
100
  <%= database_config.description %>
data/app/views/error.erb CHANGED
@@ -4,7 +4,7 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <title><%= title %></title>
7
- <link rel="icon" type="image/x-icon" href="/favicon.svg">
7
+ <link rel="icon" type="image/x-icon" href="<%= resource_path_map['favicon.svg'] %>">
8
8
  </head>
9
9
  <body style="font-family: monospace; font-size: 16px;">
10
10
 
data/app/views/sqlui.erb CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <title>SQLUI</title>
6
- <link rel="icon" type="image/x-icon" href="/favicon.svg?<%= favicon_hash %>">
6
+ <link rel="icon" type="image/x-icon" href="<%= resource_path_map['favicon.svg'] %>">
7
7
  <!-- Initialize Airbrake before loading the main app JS so that we can catch errors as early as possible. -->
8
8
  <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@airbrake/browser"></script>
9
9
  <script type="text/javascript">
@@ -19,9 +19,9 @@
19
19
  window.airbrake?.notify(error)
20
20
  }
21
21
  </script>
22
- <script type="text/javascript" src="sqlui.js?<%= js_hash %>"></script>
22
+ <script type="text/javascript" src="<%= resource_path_map['sqlui.js'] %>"></script>
23
23
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
24
- <link rel="stylesheet" href="sqlui.css?<%= css_hash %>">
24
+ <link rel="stylesheet" href="<%= resource_path_map['sqlui.css'] %>">
25
25
  </head>
26
26
 
27
27
  <body>
@@ -43,10 +43,12 @@
43
43
  </div>
44
44
 
45
45
  <div id="submit-box" class="tab-content-element graph-element query-element" style="display: none;">
46
- <input id="cancel-button" class="cancel-button" type="button" value="cancel"></input>
47
- <div class="submit-fill"></div>
46
+ <input id="cancel-button" type="button" value="cancel"></input>
47
+ <div id="editor-resizer">
48
+ <img src="<%= resource_path_map['vertical-resizer.svg'] %>" />
49
+ </div>
48
50
  <div style="position: relative;">
49
- <div class="submit-button-wrapper">
51
+ <div id="submit-button-wrapper">
50
52
  <input id="submit-button-all" class="submit-button" type="button"></input>
51
53
  <input id="submit-button-current" class="submit-button submit-button-show" type="button"></input>
52
54
  <input id="submit-dropdown-button" class="submit-dropdown-button" type="button", value="▼"></input>
@@ -120,27 +120,35 @@ p {
120
120
  padding: 5px;
121
121
  }
122
122
 
123
- .submit-fill {
123
+ #editor-resizer {
124
124
  display: flex;
125
- flex: 1
125
+ flex: 1;
126
+ cursor: row-resize;
127
+ flex-direction: column;
128
+ align-items: center;
126
129
  }
127
130
 
128
- .cancel-button {
131
+ #cancel-button {
129
132
  cursor: pointer;
130
- margin: 0;
131
133
  background: none;
132
134
  color: #333;
133
135
  border: 1px solid #888;
134
136
  height: 32px;
135
137
  font-size: 18px;
138
+ margin: 0 220px 0 0; /* To center the resizer icon. Ok, it's a hack. Get over it. */
136
139
  }
137
140
 
138
- .submit-button-wrapper {
141
+ #cancel-button-spacer {
142
+ width: 150px;
143
+ }
144
+
145
+ #submit-button-wrapper {
139
146
  cursor: pointer;
140
147
  border: 1px solid #888;
141
148
  height: 32px;
142
149
  display: flex;
143
150
  flex-direction: row;
151
+ background: white;
144
152
  }
145
153
 
146
154
  .submit-button, .submit-dropdown-button {
@@ -217,7 +225,7 @@ p {
217
225
 
218
226
  .submit-dropdown-content-button:active,
219
227
  .submit-button:active,
220
- .cancel-button:active,
228
+ #cancel-button:active,
221
229
  .submit-dropdown-button:active {
222
230
  background-color: #e6e6e6;
223
231
  outline: none
@@ -233,16 +241,15 @@ p {
233
241
  }
234
242
 
235
243
  #status-message {
236
- display: flex;
237
- justify-content: center;
238
- align-content: center;
239
- flex-direction: row;
244
+ min-width: 0;
245
+ justify-content: left;
240
246
  font-family: Helvetica, sans-serif;
241
247
  white-space: nowrap;
242
248
  overflow: hidden;
243
249
  font-size: 16px;
244
250
  color: #333;
245
- margin-left: 5px;
251
+ margin: 0 5px;
252
+ text-overflow: ellipsis;
246
253
  }
247
254
 
248
255
  #result-box, #fetch-sql-box, #saved-box, #graph-box, #structure-box {
@@ -255,6 +262,10 @@ table tbody tr td {
255
262
  height: 21px;
256
263
  }
257
264
 
265
+ #result-table td, #result-table th {
266
+ cursor: default;
267
+ }
268
+
258
269
  #result-table tbody tr td abbr a {
259
270
  color: #555;
260
271
  cursor: pointer;
@@ -264,6 +275,7 @@ table tbody tr td {
264
275
  border: 1px dotted #555;
265
276
  font-size: 12px;
266
277
  display: inline-block;
278
+ user-select: none;
267
279
  }
268
280
 
269
281
  #result-table tbody tr td abbr {
@@ -358,7 +370,8 @@ thead {
358
370
  }
359
371
 
360
372
  #status-box {
361
- padding: 5px;
373
+ width: 100%;
374
+ padding: 5px 0;
362
375
  display: flex;
363
376
  flex-direction: row;
364
377
  border-top: 1px solid #ddd;
@@ -485,11 +498,13 @@ select {
485
498
  #pagination-box {
486
499
  display: flex;
487
500
  flex-direction: row;
501
+ margin: 0 5px;
488
502
  }
489
503
 
490
504
  #page-count-box {
491
505
  align-self: center;
492
506
  font-size: 16px;
507
+ white-space: nowrap;
493
508
  }
494
509
 
495
510
  .pagination-button {