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 +4 -4
- data/.version +1 -1
- data/app/server.rb +64 -45
- data/app/sqlui.rb +1 -1
- data/client/resources/sqlui.css +9 -0
- data/client/resources/sqlui.html +3 -2
- data/client/resources/sqlui.js +78 -17
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27a06365c7119cba4bff1e78df66d107910a0cbe45422139ec34902828eda0a4
|
4
|
+
data.tar.gz: 3a1e8662d710c6b6f4d7c8331c95073f58a3365cc6d12efe32f3ef3774fb91d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 404e9bde7c78d0837c0cbdaae300e9424b88f58dc03344651ad4ac24111f96a6061db6248f49184f7c76321dfe95b599839c118564850ff382ea7567b3abc5fe
|
7
|
+
data.tar.gz: 1ad31d5edc998e57ef2be13ef6ba27b83071cd887b0438c1024895c058a0cb554ea25a1edf1cd4863f47e1e56b5c3c7f4adfa02682be4fa630f38093d3174a38
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
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
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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] =
|
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
|
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
|
-
|
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
data/client/resources/sqlui.css
CHANGED
@@ -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;
|
data/client/resources/sqlui.html
CHANGED
@@ -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>
|
data/client/resources/sqlui.js
CHANGED
@@ -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
|
24227
|
-
url.pathname = url.pathname.replace(/\/[^/]+$/, `/${
|
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
|
-
|
24279
|
+
setActionInUrl(url, 'graph');
|
24242
24280
|
document.getElementById('graph-tab-button').href = url.pathname + url.search;
|
24243
|
-
|
24281
|
+
setActionInUrl(url, 'saved');
|
24244
24282
|
document.getElementById('saved-tab-button').href = url.pathname + url.search;
|
24245
|
-
|
24283
|
+
setActionInUrl(url, 'structure');
|
24246
24284
|
document.getElementById('structure-tab-button').href = url.pathname + url.search;
|
24247
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2022-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mysql2
|