sqlui 0.1.59 → 0.1.61

Sign up to get free protection for your applications and to get access to all the features.
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 {