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