sqlui 0.1.41 → 0.1.43

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: a4307dfb13604d60040248f4a4135756f52310be559dfd3a65778e8400ab5454
4
- data.tar.gz: 00ea3b80ba354e767521943cf83058cceebd53d5d3f112fdabc09952ee375471
3
+ metadata.gz: 27a06365c7119cba4bff1e78df66d107910a0cbe45422139ec34902828eda0a4
4
+ data.tar.gz: 3a1e8662d710c6b6f4d7c8331c95073f58a3365cc6d12efe32f3ef3774fb91d5
5
5
  SHA512:
6
- metadata.gz: 1aab467177964e30d47233e5f8e60922732de9cdb0238104dc7b74eecf9271ce80e3ac449c3afaac5c4ea91863160cd7edbf352b88cc8ae11274b738abe60271
7
- data.tar.gz: 2d644c6ecd81b489acad2986be0d60f0dee3c20e5165943938279148af8f4f9f7a923a410bed570be71aad99b3f591ff92c050cd2a27df9ae06bdbb4b63ef8d7
6
+ metadata.gz: 404e9bde7c78d0837c0cbdaae300e9424b88f58dc03344651ad4ac24111f96a6061db6248f49184f7c76321dfe95b599839c118564850ff382ea7567b3abc5fe
7
+ data.tar.gz: 1ad31d5edc998e57ef2be13ef6ba27b83071cd887b0438c1024895c058a0cb554ea25a1edf1cd4863f47e1e56b5c3c7f4adfa02682be4fa630f38093d3174a38
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.41
1
+ 0.1.43
data/app/server.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
4
+ require 'csv'
3
5
  require 'erb'
4
6
  require 'json'
5
7
  require 'sinatra/base'
6
8
  require 'uri'
9
+ require 'webrick/log'
7
10
  require_relative 'database_metadata'
8
11
  require_relative 'mysql_types'
9
12
  require_relative 'sql_parser'
@@ -24,6 +27,8 @@ class Server < Sinatra::Base
24
27
  set :raise_errors, false
25
28
  set :show_exceptions, false
26
29
 
30
+ logger = WEBrick::Log.new
31
+
27
32
  get '/-/health' do
28
33
  status 200
29
34
  body 'OK'
@@ -91,43 +96,54 @@ class Server < Sinatra::Base
91
96
  params.merge!(JSON.parse(request.body.read, symbolize_names: true))
92
97
  break client_error('missing sql') unless params[:sql]
93
98
 
94
- full_sql = params[:sql]
95
- sql = params[:sql]
96
99
  variables = params[:variables] || {}
97
- if params[:selection]
98
- selection = params[:selection]
99
- if selection.include?('-')
100
- # sort because the selection could be in either direction
101
- selection = params[:selection].split('-').map { |v| Integer(v) }.sort
102
- else
103
- selection = Integer(selection)
104
- selection = [selection, selection]
105
- end
106
-
107
- sql = if selection[0] == selection[1]
108
- SqlParser.find_statement_at_cursor(params[:sql], selection[0])
109
- else
110
- full_sql[selection[0], selection[1]]
111
- end
112
-
113
- break client_error("can't find query at selection") unless sql
114
- end
100
+ sql = find_selected_query(params[:sql], params[:selection])
115
101
 
116
102
  result = database.with_client do |client|
117
- variables.each do |name, value|
118
- client.query("SET @#{name} = #{value};")
119
- end
120
- execute_query(client, sql)
103
+ query_result = execute_query(client, variables, sql)
104
+ # NOTE: the call to result.field_types must go before other interaction with the result. Otherwise you will
105
+ # get a seg fault. Seems to be a bug in Mysql2.
106
+ # TODO: stream this and render results on the client as they are returned?
107
+ {
108
+ columns: query_result.fields,
109
+ column_types: MysqlTypes.map_to_google_charts_types(query_result.field_types),
110
+ total_rows: query_result.size,
111
+ rows: (query_result.to_a || []).take(Sqlui::MAX_ROWS)
112
+ }
121
113
  end
122
114
 
123
115
  result[:selection] = params[:selection]
124
- result[:query] = full_sql
116
+ result[:query] = params[:sql]
125
117
 
126
118
  status 200
127
119
  headers 'Content-Type' => 'application/json; charset=utf-8'
128
120
  body result.to_json
129
121
  end
130
122
 
123
+ get "#{database.url_path}/download_csv" do
124
+ break client_error('missing sql') unless params[:sql]
125
+
126
+ sql = Base64.decode64(params[:sql]).force_encoding('UTF-8')
127
+ logger.info "sql: #{sql}"
128
+ variables = params.map { |k, v| k[0] == '_' ? [k, v] : nil }.compact.to_h
129
+ sql = find_selected_query(sql, params[:selection])
130
+ logger.info "sql: #{sql}"
131
+
132
+ content_type 'application/csv; charset=utf-8'
133
+ attachment 'result.csv'
134
+ status 200
135
+
136
+ database.with_client do |client|
137
+ query_result = execute_query(client, variables, sql)
138
+ stream do |out|
139
+ out << CSV::Row.new(query_result.fields, query_result.fields, header_row: true).to_s.strip
140
+ query_result.each do |row|
141
+ out << "\n#{CSV::Row.new(query_result.fields, row).to_s.strip}"
142
+ end
143
+ end
144
+ end
145
+ end
146
+
131
147
  get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
132
148
  @html ||= File.read(File.join(resources_dir, 'sqlui.html'))
133
149
  status 200
@@ -158,30 +174,33 @@ class Server < Sinatra::Base
158
174
  body({ error: message, stacktrace: stacktrace }.compact.to_json)
159
175
  end
160
176
 
161
- def execute_query(client, sql)
177
+ def find_selected_query(full_sql, selection)
178
+ return full_sql unless selection
179
+
180
+ if selection.include?('-')
181
+ # sort because the selection could be in either direction
182
+ selection = selection.split('-').map { |v| Integer(v) }.sort
183
+ else
184
+ selection = Integer(selection)
185
+ selection = [selection, selection]
186
+ end
187
+
188
+ if selection[0] == selection[1]
189
+ SqlParser.find_statement_at_cursor(full_sql, selection[0])
190
+ else
191
+ full_sql[selection[0], selection[1]]
192
+ end
193
+ end
194
+
195
+ def execute_query(client, variables, sql)
196
+ variables.each do |name, value|
197
+ client.query("SET @#{name} = #{value};")
198
+ end
162
199
  queries = if sql.include?(';')
163
200
  sql.split(/(?<=;)/).map(&:strip).reject(&:empty?)
164
201
  else
165
202
  [sql]
166
203
  end
167
- results = queries.map { |current| client.query(current) }
168
- result = results[-1]
169
- # NOTE: the call to result.field_types must go before any other interaction with the result. Otherwise you will
170
- # get a seg fault. Seems to be a bug in Mysql2.
171
- if result
172
- column_types = MysqlTypes.map_to_google_charts_types(result.field_types)
173
- rows = result.to_a
174
- columns = result.fields
175
- else
176
- column_types = []
177
- rows = []
178
- columns = []
179
- end
180
- {
181
- columns: columns,
182
- column_types: column_types,
183
- total_rows: rows.size,
184
- rows: rows.take(Sqlui::MAX_ROWS)
185
- }
204
+ queries.map { |current| client.query(current) }.last
186
205
  end
187
206
  end
data/app/sqlui.rb CHANGED
@@ -5,7 +5,7 @@ require_relative 'server'
5
5
 
6
6
  # Main entry point.
7
7
  class Sqlui
8
- MAX_ROWS = 1_000
8
+ MAX_ROWS = 10_000
9
9
 
10
10
  def initialize(config_file)
11
11
  raise 'you must specify a configuration file' unless config_file
@@ -218,6 +218,15 @@ p {
218
218
  outline: none
219
219
  }
220
220
 
221
+ .submit-dropdown-content-button.disabled {
222
+ color: #888 !important;
223
+ cursor: auto !important;
224
+ }
225
+
226
+ .submit-dropdown-content-button.disabled:hover {
227
+ background: none !important;
228
+ }
229
+
221
230
  .status {
222
231
  display: flex;
223
232
  justify-content: center;
@@ -41,8 +41,9 @@
41
41
  <input id="submit-dropdown-button-toggle" class="submit-dropdown-content-button" type="button" value="toggle default"></input>
42
42
  </div>
43
43
  <div class="submit-dropdown-content-section">
44
- <input id="submit-dropdown-button-copy-csv" class="submit-dropdown-content-button" type="button" value="copy to clipboard (csv)"></input>
45
- <input id="submit-dropdown-button-copy-tsv" class="submit-dropdown-content-button" type="button" value="copy to clipboard (tsv)"></input>
44
+ <input id="submit-dropdown-button-copy-csv" class="submit-dropdown-content-button disabled" type="button" value="copy to clipboard (csv)"></input>
45
+ <input id="submit-dropdown-button-copy-tsv" class="submit-dropdown-content-button disabled" type="button" value="copy to clipboard (tsv)"></input>
46
+ <input id="submit-dropdown-button-download-csv" class="submit-dropdown-content-button disabled" type="button" value="download (csv)"></input>
46
47
  </div>
47
48
  </div>
48
49
  </div>
@@ -23271,7 +23271,7 @@
23271
23271
  LineComment = 1,
23272
23272
  BlockComment = 2,
23273
23273
  String$1 = 3,
23274
- Number = 4,
23274
+ Number$1 = 4,
23275
23275
  Bool = 5,
23276
23276
  Null = 6,
23277
23277
  ParenL = 7,
@@ -23540,18 +23540,18 @@
23540
23540
  input.advance();
23541
23541
  if (quoted && input.next == 39 /* Ch.SingleQuote */)
23542
23542
  input.advance();
23543
- input.acceptToken(Number);
23543
+ input.acceptToken(Number$1);
23544
23544
  }
23545
23545
  else if (next == 46 /* Ch.Dot */ && input.next >= 48 /* Ch._0 */ && input.next <= 57 /* Ch._9 */) {
23546
23546
  readNumber(input, true);
23547
- input.acceptToken(Number);
23547
+ input.acceptToken(Number$1);
23548
23548
  }
23549
23549
  else if (next == 46 /* Ch.Dot */) {
23550
23550
  input.acceptToken(Dot);
23551
23551
  }
23552
23552
  else if (next >= 48 /* Ch._0 */ && next <= 57 /* Ch._9 */) {
23553
23553
  readNumber(input, false);
23554
- input.acceptToken(Number);
23554
+ input.acceptToken(Number$1);
23555
23555
  }
23556
23556
  else if (inString(next, d.operatorChars)) {
23557
23557
  while (inString(input.next, d.operatorChars))
@@ -23867,6 +23867,31 @@
23867
23867
  return match ? match[1] : identifier
23868
23868
  }
23869
23869
 
23870
+ function base64Encode (str) {
23871
+ // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
23872
+ return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
23873
+ function toSolidBytes (match, p1) {
23874
+ return String.fromCharCode(Number(`0x${p1}`))
23875
+ }))
23876
+ }
23877
+
23878
+ function getSqlFromUrl (url) {
23879
+ const params = url.searchParams;
23880
+ if (params.has('file') && params.has('sql')) {
23881
+ // TODO: show an error.
23882
+ throw new Error('You can only specify a file or sql param, not both.')
23883
+ }
23884
+ if (params.has('sql')) {
23885
+ return params.get('sql')
23886
+ } else if (params.has('file')) {
23887
+ const file = params.get('file');
23888
+ const fileDetails = window.metadata.saved[file];
23889
+ if (!fileDetails) throw new Error(`no such file: ${file}`)
23890
+ return fileDetails.contents
23891
+ }
23892
+ throw new Error('You must specify a file or sql param')
23893
+ }
23894
+
23870
23895
  function init (parent, onSubmit, onShiftSubmit) {
23871
23896
  addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
23872
23897
  addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
@@ -23907,9 +23932,7 @@
23907
23932
 
23908
23933
  const copyListenerFactory = (delimiter) => {
23909
23934
  return () => {
23910
- if (
23911
- !window.sqlFetch?.result
23912
- ) {
23935
+ if (!window.sqlFetch?.result) {
23913
23936
  return
23914
23937
  }
23915
23938
  const type = 'text/plain';
@@ -23935,6 +23958,21 @@
23935
23958
  };
23936
23959
  addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), copyListenerFactory(','));
23937
23960
  addClickListener(document.getElementById('submit-dropdown-button-copy-tsv'), copyListenerFactory('\t'));
23961
+ addClickListener(document.getElementById('submit-dropdown-button-download-csv'), () => {
23962
+ if (!window.sqlFetch?.result) return
23963
+
23964
+ const url = new URL(window.location);
23965
+ url.searchParams.set('sql', base64Encode(getSqlFromUrl(url)));
23966
+ url.searchParams.delete('file');
23967
+ setActionInUrl(url, 'download_csv');
23968
+
23969
+ const link = document.createElement('a');
23970
+ link.setAttribute('download', 'result.csv');
23971
+ link.setAttribute('href', url.href);
23972
+ link.click();
23973
+
23974
+ focus(getSelection());
23975
+ });
23938
23976
 
23939
23977
  document.addEventListener('click', function (event) {
23940
23978
  if (event.target !== dropdownButton) {
@@ -24223,8 +24261,8 @@
24223
24261
  });
24224
24262
  }
24225
24263
 
24226
- function setTabInUrl (url, tab) {
24227
- url.pathname = url.pathname.replace(/\/[^/]+$/, `/${tab}`);
24264
+ function setActionInUrl (url, action) {
24265
+ url.pathname = url.pathname.replace(/\/[^/]+$/, `/${action}`);
24228
24266
  }
24229
24267
 
24230
24268
  function getTabFromUrl (url) {
@@ -24238,19 +24276,19 @@
24238
24276
 
24239
24277
  function updateTabs () {
24240
24278
  const url = new URL(window.location);
24241
- setTabInUrl(url, 'graph');
24279
+ setActionInUrl(url, 'graph');
24242
24280
  document.getElementById('graph-tab-button').href = url.pathname + url.search;
24243
- setTabInUrl(url, 'saved');
24281
+ setActionInUrl(url, 'saved');
24244
24282
  document.getElementById('saved-tab-button').href = url.pathname + url.search;
24245
- setTabInUrl(url, 'structure');
24283
+ setActionInUrl(url, 'structure');
24246
24284
  document.getElementById('structure-tab-button').href = url.pathname + url.search;
24247
- setTabInUrl(url, 'query');
24285
+ setActionInUrl(url, 'query');
24248
24286
  document.getElementById('query-tab-button').href = url.pathname + url.search;
24249
24287
  }
24250
24288
 
24251
24289
  function selectTab (event, tab) {
24252
24290
  const url = new URL(window.location);
24253
- setTabInUrl(url, tab);
24291
+ setActionInUrl(url, tab);
24254
24292
  route(event.target, event, url, true);
24255
24293
  }
24256
24294
 
@@ -24459,6 +24497,7 @@
24459
24497
  document.getElementById('graph-status').style.display = 'flex';
24460
24498
  document.getElementById('fetch-sql-box').style.display = 'none';
24461
24499
  document.getElementById('cancel-button').style.display = 'none';
24500
+ updateDownloadButtons(window?.sqlFetch);
24462
24501
  maybeFetchResult(internal);
24463
24502
 
24464
24503
  focus(getSelection());
@@ -24493,7 +24532,7 @@
24493
24532
  setSavedStatus(`${numFiles} file${numFiles === 1 ? '' : 's'}`);
24494
24533
  Object.values(saved).forEach(file => {
24495
24534
  const viewUrl = new URL(window.location.origin + window.location.pathname);
24496
- setTabInUrl(viewUrl, 'query');
24535
+ setActionInUrl(viewUrl, 'query');
24497
24536
  viewUrl.searchParams.set('file', file.filename);
24498
24537
 
24499
24538
  const viewLinkElement = document.createElement('a');
@@ -24506,7 +24545,7 @@
24506
24545
  });
24507
24546
 
24508
24547
  const runUrl = new URL(window.location.origin + window.location.pathname);
24509
- setTabInUrl(runUrl, 'query');
24548
+ setActionInUrl(runUrl, 'query');
24510
24549
  runUrl.searchParams.set('file', file.filename);
24511
24550
  runUrl.searchParams.set('run', 'true');
24512
24551
 
@@ -24607,6 +24646,7 @@
24607
24646
  clearGraphStatus();
24608
24647
  clearResultBox();
24609
24648
  clearResultStatus();
24649
+ disableDownloadButtons();
24610
24650
  }
24611
24651
 
24612
24652
  function clearResultStatus () {
@@ -24876,7 +24916,28 @@
24876
24916
  });
24877
24917
  }
24878
24918
 
24919
+ function disableDownloadButtons () {
24920
+ document.getElementById('submit-dropdown-button-download-csv').classList.add('disabled');
24921
+ document.getElementById('submit-dropdown-button-copy-csv').classList.add('disabled');
24922
+ document.getElementById('submit-dropdown-button-copy-tsv').classList.add('disabled');
24923
+ }
24924
+
24925
+ function enableDownloadButtons () {
24926
+ document.getElementById('submit-dropdown-button-download-csv').classList.remove('disabled');
24927
+ document.getElementById('submit-dropdown-button-copy-csv').classList.remove('disabled');
24928
+ document.getElementById('submit-dropdown-button-copy-tsv').classList.remove('disabled');
24929
+ }
24930
+
24931
+ function updateDownloadButtons (fetch) {
24932
+ if (fetch?.state === 'success') {
24933
+ enableDownloadButtons();
24934
+ } else {
24935
+ disableDownloadButtons();
24936
+ }
24937
+ }
24938
+
24879
24939
  function displaySqlFetch (fetch) {
24940
+ updateDownloadButtons(fetch);
24880
24941
  if (window.tab === 'query') {
24881
24942
  displaySqlFetchInResultTab(fetch);
24882
24943
  } else if (window.tab === 'graph') {
@@ -25005,7 +25066,7 @@
25005
25066
  if (result.total_rows === 1) {
25006
25067
  statusElement.innerText = `${result.total_rows} row (${elapsed}s)`;
25007
25068
  } else {
25008
- statusElement.innerText = `${result.total_rows} rows (${elapsed}s)`;
25069
+ statusElement.innerText = `${result.total_rows.toLocaleString()} rows (${elapsed}s)`;
25009
25070
  }
25010
25071
 
25011
25072
  if (result.total_rows > result.rows.length) {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.41
4
+ version: 0.1.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Dower
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-18 00:00:00.000000000 Z
11
+ date: 2022-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2