sqlui 0.1.41 → 0.1.43

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: 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