sqlui 0.1.40 → 0.1.42
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/args.rb +4 -0
- data/app/database_config.rb +11 -1
- data/app/database_metadata.rb +20 -22
- data/app/server.rb +70 -45
- data/app/sqlui.rb +1 -1
- data/app/sqlui_config.rb +1 -1
- data/client/resources/sqlui.css +9 -0
- data/client/resources/sqlui.html +3 -2
- data/client/resources/sqlui.js +164 -48
- 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: 1bf6c0615e6ef20a8ccdb39cb45a50dbc1ce2a5ed465f0c92de054d63577c27b
|
|
4
|
+
data.tar.gz: ff7a3de7803138292bf86594ce224c09557156189486212d5aee0b83447d8553
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9db790262959d363d28b658148931b3e98aedb03f4f728f3c0a268e111a6d9b038b591949680bc5abf3dcf1118652d0cc4489109a478c4527bdb951590e49e35
|
|
7
|
+
data.tar.gz: 9771a25e3c63baeeb5fc3a8d71b275496c96656f2f12fac2996d17b2fc8a82b1666932d1b8fc11757bf2b54666f03eec2173cbcb11de420e7e72be8e8a2609b5
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.42
|
data/app/args.rb
CHANGED
|
@@ -24,6 +24,10 @@ class Args
|
|
|
24
24
|
fetch_optional(hash, key, Hash)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def self.fetch_optional_array(hash, key)
|
|
28
|
+
fetch_optional(hash, key, Array)
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
def self.fetch_non_nil(hash, key, *classes)
|
|
28
32
|
raise ArgumentError, "required parameter #{key} missing" unless hash.key?(key)
|
|
29
33
|
|
data/app/database_config.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require 'mysql2'
|
|
4
5
|
require 'set'
|
|
5
6
|
|
|
@@ -7,7 +8,7 @@ require_relative 'args'
|
|
|
7
8
|
|
|
8
9
|
# Config for a single database.
|
|
9
10
|
class DatabaseConfig
|
|
10
|
-
attr_reader :display_name, :description, :url_path, :saved_path, :table_aliases, :client_params
|
|
11
|
+
attr_reader :display_name, :description, :url_path, :joins, :saved_path, :table_aliases, :client_params
|
|
11
12
|
|
|
12
13
|
def initialize(hash)
|
|
13
14
|
@display_name = Args.fetch_non_empty_string(hash, :display_name).strip
|
|
@@ -17,6 +18,15 @@ class DatabaseConfig
|
|
|
17
18
|
raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
|
|
18
19
|
|
|
19
20
|
@saved_path = Args.fetch_non_empty_string(hash, :saved_path).strip
|
|
21
|
+
@joins = Args.fetch_optional_array(hash, :joins) || []
|
|
22
|
+
@joins.map do |join|
|
|
23
|
+
next if join.is_a?(Hash) &&
|
|
24
|
+
join.keys.size == 2 &&
|
|
25
|
+
join[:label].is_a?(String) && !join[:label].strip.empty? &&
|
|
26
|
+
join[:apply].is_a?(String) && !join[:apply].strip.empty?
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, "invalid join #{join.to_json}"
|
|
29
|
+
end
|
|
20
30
|
@table_aliases = Args.fetch_optional_hash(hash, :table_aliases) || {}
|
|
21
31
|
@table_aliases = @table_aliases.each do |table, a|
|
|
22
32
|
raise ArgumentError, "invalid alias for table #{table} (#{a}), expected string" unless a.is_a?(String)
|
data/app/database_metadata.rb
CHANGED
|
@@ -38,18 +38,17 @@ class DatabaseMetadata
|
|
|
38
38
|
extra
|
|
39
39
|
from information_schema.columns
|
|
40
40
|
#{where_clause}
|
|
41
|
-
order by table_schema, table_name,
|
|
41
|
+
order by table_schema, table_name, ordinal_position;
|
|
42
42
|
SQL
|
|
43
43
|
)
|
|
44
|
-
column_result.each do |row|
|
|
45
|
-
|
|
46
|
-
table_schema = row[:table_schema]
|
|
44
|
+
column_result.to_a.each do |row|
|
|
45
|
+
table_schema = row.shift
|
|
47
46
|
unless result[table_schema]
|
|
48
47
|
result[table_schema] = {
|
|
49
48
|
tables: {}
|
|
50
49
|
}
|
|
51
50
|
end
|
|
52
|
-
table_name = row
|
|
51
|
+
table_name = row.shift
|
|
53
52
|
tables = result[table_schema][:tables]
|
|
54
53
|
unless tables[table_name]
|
|
55
54
|
tables[table_name] = {
|
|
@@ -58,16 +57,16 @@ class DatabaseMetadata
|
|
|
58
57
|
}
|
|
59
58
|
end
|
|
60
59
|
columns = result[table_schema][:tables][table_name][:columns]
|
|
61
|
-
column_name = row
|
|
60
|
+
column_name = row.shift
|
|
62
61
|
columns[column_name] = {} unless columns[column_name]
|
|
63
62
|
column = columns[column_name]
|
|
64
63
|
column[:name] = column_name
|
|
65
|
-
column[:data_type] = row
|
|
66
|
-
column[:length] = row
|
|
67
|
-
column[:allow_null] = row
|
|
68
|
-
column[:key] = row
|
|
69
|
-
column[:default] = row
|
|
70
|
-
column[:extra] = row
|
|
64
|
+
column[:data_type] = row.shift
|
|
65
|
+
column[:length] = row.shift
|
|
66
|
+
column[:allow_null] = row.shift
|
|
67
|
+
column[:key] = row.shift
|
|
68
|
+
column[:default] = row.shift
|
|
69
|
+
column[:extra] = row.shift
|
|
71
70
|
end
|
|
72
71
|
result
|
|
73
72
|
end
|
|
@@ -86,30 +85,29 @@ class DatabaseMetadata
|
|
|
86
85
|
table_schema,
|
|
87
86
|
table_name,
|
|
88
87
|
index_name,
|
|
88
|
+
column_name,
|
|
89
89
|
seq_in_index,
|
|
90
|
-
non_unique
|
|
91
|
-
column_name
|
|
90
|
+
non_unique
|
|
92
91
|
from information_schema.statistics
|
|
93
92
|
#{where_clause}
|
|
94
93
|
order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
|
|
95
94
|
SQL
|
|
96
95
|
)
|
|
97
96
|
stats_result.each do |row|
|
|
98
|
-
|
|
99
|
-
table_schema = row[:table_schema]
|
|
97
|
+
table_schema = row.shift
|
|
100
98
|
tables = result[table_schema][:tables]
|
|
101
|
-
table_name = row
|
|
99
|
+
table_name = row.shift
|
|
102
100
|
indexes = tables[table_name][:indexes]
|
|
103
|
-
index_name = row
|
|
101
|
+
index_name = row.shift
|
|
104
102
|
indexes[index_name] = {} unless indexes[index_name]
|
|
105
103
|
index = indexes[index_name]
|
|
106
|
-
column_name = row
|
|
104
|
+
column_name = row.shift
|
|
107
105
|
index[column_name] = {}
|
|
108
106
|
column = index[column_name]
|
|
109
107
|
column[:name] = index_name
|
|
110
|
-
column[:seq_in_index] = row
|
|
111
|
-
column[:non_unique] = row
|
|
112
|
-
column[:column_name] =
|
|
108
|
+
column[:seq_in_index] = row.shift
|
|
109
|
+
column[:non_unique] = row.shift
|
|
110
|
+
column[:column_name] = column_name
|
|
113
111
|
end
|
|
114
112
|
end
|
|
115
113
|
end
|
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'
|
|
@@ -12,6 +15,11 @@ require_relative 'sqlui'
|
|
|
12
15
|
# SQLUI Sinatra server.
|
|
13
16
|
class Server < Sinatra::Base
|
|
14
17
|
def self.init_and_run(config, resources_dir)
|
|
18
|
+
Mysql2::Client.default_query_options[:as] = :array
|
|
19
|
+
Mysql2::Client.default_query_options[:cast_booleans] = true
|
|
20
|
+
Mysql2::Client.default_query_options[:database_timezone] = :utc
|
|
21
|
+
Mysql2::Client.default_query_options[:cache_rows] = false
|
|
22
|
+
|
|
15
23
|
set :logging, true
|
|
16
24
|
set :bind, '0.0.0.0'
|
|
17
25
|
set :port, config.port
|
|
@@ -19,6 +27,8 @@ class Server < Sinatra::Base
|
|
|
19
27
|
set :raise_errors, false
|
|
20
28
|
set :show_exceptions, false
|
|
21
29
|
|
|
30
|
+
logger = WEBrick::Log.new
|
|
31
|
+
|
|
22
32
|
get '/-/health' do
|
|
23
33
|
status 200
|
|
24
34
|
body 'OK'
|
|
@@ -58,6 +68,7 @@ class Server < Sinatra::Base
|
|
|
58
68
|
list_url_path: config.list_url_path,
|
|
59
69
|
schemas: DatabaseMetadata.lookup(client, database),
|
|
60
70
|
table_aliases: database.table_aliases,
|
|
71
|
+
joins: database.joins,
|
|
61
72
|
saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
|
|
62
73
|
contents = File.read(path)
|
|
63
74
|
comment_lines = contents.split("\n").take_while do |l|
|
|
@@ -85,43 +96,54 @@ class Server < Sinatra::Base
|
|
|
85
96
|
params.merge!(JSON.parse(request.body.read, symbolize_names: true))
|
|
86
97
|
break client_error('missing sql') unless params[:sql]
|
|
87
98
|
|
|
88
|
-
full_sql = params[:sql]
|
|
89
|
-
sql = params[:sql]
|
|
90
99
|
variables = params[:variables] || {}
|
|
91
|
-
|
|
92
|
-
selection = params[:selection]
|
|
93
|
-
if selection.include?('-')
|
|
94
|
-
# sort because the selection could be in either direction
|
|
95
|
-
selection = params[:selection].split('-').map { |v| Integer(v) }.sort
|
|
96
|
-
else
|
|
97
|
-
selection = Integer(selection)
|
|
98
|
-
selection = [selection, selection]
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
sql = if selection[0] == selection[1]
|
|
102
|
-
SqlParser.find_statement_at_cursor(params[:sql], selection[0])
|
|
103
|
-
else
|
|
104
|
-
full_sql[selection[0], selection[1]]
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
break client_error("can't find query at selection") unless sql
|
|
108
|
-
end
|
|
100
|
+
sql = find_selected_query(params[:sql], params[:selection])
|
|
109
101
|
|
|
110
102
|
result = database.with_client do |client|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
115
113
|
end
|
|
116
114
|
|
|
117
115
|
result[:selection] = params[:selection]
|
|
118
|
-
result[:query] =
|
|
116
|
+
result[:query] = params[:sql]
|
|
119
117
|
|
|
120
118
|
status 200
|
|
121
119
|
headers 'Content-Type' => 'application/json; charset=utf-8'
|
|
122
120
|
body result.to_json
|
|
123
121
|
end
|
|
124
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
|
+
|
|
125
147
|
get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
|
|
126
148
|
@html ||= File.read(File.join(resources_dir, 'sqlui.html'))
|
|
127
149
|
status 200
|
|
@@ -152,30 +174,33 @@ class Server < Sinatra::Base
|
|
|
152
174
|
body({ error: message, stacktrace: stacktrace }.compact.to_json)
|
|
153
175
|
end
|
|
154
176
|
|
|
155
|
-
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
|
|
156
199
|
queries = if sql.include?(';')
|
|
157
200
|
sql.split(/(?<=;)/).map(&:strip).reject(&:empty?)
|
|
158
201
|
else
|
|
159
202
|
[sql]
|
|
160
203
|
end
|
|
161
|
-
|
|
162
|
-
result = results[-1]
|
|
163
|
-
# NOTE: the call to result.field_types must go before any other interaction with the result. Otherwise you will
|
|
164
|
-
# get a seg fault. Seems to be a bug in Mysql2.
|
|
165
|
-
if result
|
|
166
|
-
column_types = MysqlTypes.map_to_google_charts_types(result.field_types)
|
|
167
|
-
rows = result.map(&:values)
|
|
168
|
-
columns = result.first&.keys || []
|
|
169
|
-
else
|
|
170
|
-
column_types = []
|
|
171
|
-
rows = []
|
|
172
|
-
columns = []
|
|
173
|
-
end
|
|
174
|
-
{
|
|
175
|
-
columns: columns,
|
|
176
|
-
column_types: column_types,
|
|
177
|
-
total_rows: rows.size,
|
|
178
|
-
rows: rows.take(Sqlui::MAX_ROWS)
|
|
179
|
-
}
|
|
204
|
+
queries.map { |current| client.query(current) }.last
|
|
180
205
|
end
|
|
181
206
|
end
|
data/app/sqlui.rb
CHANGED
data/app/sqlui_config.rb
CHANGED
|
@@ -11,7 +11,7 @@ class SqluiConfig
|
|
|
11
11
|
attr_reader :name, :port, :environment, :list_url_path, :database_configs
|
|
12
12
|
|
|
13
13
|
def initialize(filename, overrides = {})
|
|
14
|
-
config = YAML.safe_load(ERB.new(File.read(filename)).result).deep_merge!(overrides)
|
|
14
|
+
config = YAML.safe_load(ERB.new(File.read(filename)).result, aliases: true).deep_merge!(overrides)
|
|
15
15
|
config.deep_symbolize_keys!
|
|
16
16
|
@name = Args.fetch_non_empty_string(config, :name).strip
|
|
17
17
|
@port = Args.fetch_non_empty_int(config, :port)
|
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
|
@@ -3359,23 +3359,24 @@
|
|
|
3359
3359
|
*/
|
|
3360
3360
|
minPointSize = -1) {
|
|
3361
3361
|
let cursor = new SpanCursor(sets, null, minPointSize).goto(from), pos = from;
|
|
3362
|
-
let
|
|
3362
|
+
let openRanges = cursor.openStart;
|
|
3363
3363
|
for (;;) {
|
|
3364
3364
|
let curTo = Math.min(cursor.to, to);
|
|
3365
3365
|
if (cursor.point) {
|
|
3366
|
-
|
|
3367
|
-
|
|
3366
|
+
let active = cursor.activeForPoint(cursor.to);
|
|
3367
|
+
let openCount = cursor.pointFrom < from ? active.length + 1 : Math.min(active.length, openRanges);
|
|
3368
|
+
iterator.point(pos, curTo, cursor.point, active, openCount, cursor.pointRank);
|
|
3369
|
+
openRanges = Math.min(cursor.openEnd(curTo), active.length);
|
|
3368
3370
|
}
|
|
3369
3371
|
else if (curTo > pos) {
|
|
3370
|
-
iterator.span(pos, curTo, cursor.active,
|
|
3371
|
-
|
|
3372
|
+
iterator.span(pos, curTo, cursor.active, openRanges);
|
|
3373
|
+
openRanges = cursor.openEnd(curTo);
|
|
3372
3374
|
}
|
|
3373
3375
|
if (cursor.to > to)
|
|
3374
|
-
|
|
3376
|
+
return openRanges + (cursor.point && cursor.to > to ? 1 : 0);
|
|
3375
3377
|
pos = cursor.to;
|
|
3376
3378
|
cursor.next();
|
|
3377
3379
|
}
|
|
3378
|
-
return open;
|
|
3379
3380
|
}
|
|
3380
3381
|
/**
|
|
3381
3382
|
Create a range set for the given range or array of ranges. By
|
|
@@ -3679,6 +3680,8 @@
|
|
|
3679
3680
|
this.pointRank = 0;
|
|
3680
3681
|
this.to = -1000000000 /* C.Far */;
|
|
3681
3682
|
this.endSide = 0;
|
|
3683
|
+
// The amount of open active ranges at the start of the iterator.
|
|
3684
|
+
// Not including points.
|
|
3682
3685
|
this.openStart = -1;
|
|
3683
3686
|
this.cursor = HeapCursor.from(sets, skip, minPoint);
|
|
3684
3687
|
}
|
|
@@ -3719,7 +3722,7 @@
|
|
|
3719
3722
|
next() {
|
|
3720
3723
|
let from = this.to, wasPoint = this.point;
|
|
3721
3724
|
this.point = null;
|
|
3722
|
-
let trackOpen = this.openStart < 0 ? [] : null
|
|
3725
|
+
let trackOpen = this.openStart < 0 ? [] : null;
|
|
3723
3726
|
for (;;) {
|
|
3724
3727
|
let a = this.minActive;
|
|
3725
3728
|
if (a > -1 && (this.activeTo[a] - this.cursor.from || this.active[a].endSide - this.cursor.startSide) < 0) {
|
|
@@ -3745,8 +3748,6 @@
|
|
|
3745
3748
|
let nextVal = this.cursor.value;
|
|
3746
3749
|
if (!nextVal.point) { // Opening a range
|
|
3747
3750
|
this.addActive(trackOpen);
|
|
3748
|
-
if (this.cursor.from < from && this.cursor.to > from)
|
|
3749
|
-
trackExtra++;
|
|
3750
3751
|
this.cursor.next();
|
|
3751
3752
|
}
|
|
3752
3753
|
else if (wasPoint && this.cursor.to == this.to && this.cursor.from < this.cursor.to) {
|
|
@@ -3759,8 +3760,6 @@
|
|
|
3759
3760
|
this.pointRank = this.cursor.rank;
|
|
3760
3761
|
this.to = this.cursor.to;
|
|
3761
3762
|
this.endSide = nextVal.endSide;
|
|
3762
|
-
if (this.cursor.from < from)
|
|
3763
|
-
trackExtra = 1;
|
|
3764
3763
|
this.cursor.next();
|
|
3765
3764
|
this.forward(this.to, this.endSide);
|
|
3766
3765
|
break;
|
|
@@ -3768,10 +3767,9 @@
|
|
|
3768
3767
|
}
|
|
3769
3768
|
}
|
|
3770
3769
|
if (trackOpen) {
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
openStart++;
|
|
3774
|
-
this.openStart = openStart + trackExtra;
|
|
3770
|
+
this.openStart = 0;
|
|
3771
|
+
for (let i = trackOpen.length - 1; i >= 0 && trackOpen[i] < from; i--)
|
|
3772
|
+
this.openStart++;
|
|
3775
3773
|
}
|
|
3776
3774
|
}
|
|
3777
3775
|
activeForPoint(to) {
|
|
@@ -5826,7 +5824,7 @@
|
|
|
5826
5824
|
}
|
|
5827
5825
|
}
|
|
5828
5826
|
let take = Math.min(this.text.length - this.textOff, length, 512 /* T.Chunk */);
|
|
5829
|
-
this.flushBuffer(active.slice(
|
|
5827
|
+
this.flushBuffer(active.slice(active.length - openStart));
|
|
5830
5828
|
this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
|
|
5831
5829
|
this.atCursorPos = true;
|
|
5832
5830
|
this.textOff += take;
|
|
@@ -20367,6 +20365,8 @@
|
|
|
20367
20365
|
if (tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) ||
|
|
20368
20366
|
!sameResults(active, this.active))
|
|
20369
20367
|
open = CompletionDialog.build(active, state, this.id, this.open, conf);
|
|
20368
|
+
else if (open && open.disabled && !active.some(a => a.state == 1 /* State.Pending */))
|
|
20369
|
+
open = null;
|
|
20370
20370
|
else if (open && tr.docChanged)
|
|
20371
20371
|
open = open.map(tr.changes);
|
|
20372
20372
|
if (!open && active.every(a => a.state != 1 /* State.Pending */) && active.some(a => a.hasResult()))
|
|
@@ -21292,7 +21292,7 @@
|
|
|
21292
21292
|
- F8: [`nextDiagnostic`](https://codemirror.net/6/docs/ref/#lint.nextDiagnostic)
|
|
21293
21293
|
*/
|
|
21294
21294
|
const lintKeymap = [
|
|
21295
|
-
{ key: "Mod-Shift-m", run: openLintPanel },
|
|
21295
|
+
{ key: "Mod-Shift-m", run: openLintPanel, preventDefault: true },
|
|
21296
21296
|
{ key: "F8", run: nextDiagnostic }
|
|
21297
21297
|
];
|
|
21298
21298
|
const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
|
|
@@ -21916,10 +21916,10 @@
|
|
|
21916
21916
|
canShift(term) {
|
|
21917
21917
|
for (let sim = new SimulatedStack(this);;) {
|
|
21918
21918
|
let action = this.p.parser.stateSlot(sim.state, 4 /* DefaultReduce */) || this.p.parser.hasAction(sim.state, term);
|
|
21919
|
-
if ((action & 65536 /* ReduceFlag */) == 0)
|
|
21920
|
-
return true;
|
|
21921
21919
|
if (action == 0)
|
|
21922
21920
|
return false;
|
|
21921
|
+
if ((action & 65536 /* ReduceFlag */) == 0)
|
|
21922
|
+
return true;
|
|
21923
21923
|
sim.reduce(action);
|
|
21924
21924
|
}
|
|
21925
21925
|
}
|
|
@@ -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))
|
|
@@ -23862,6 +23862,36 @@
|
|
|
23862
23862
|
|
|
23863
23863
|
/* global google */
|
|
23864
23864
|
|
|
23865
|
+
function unquoteSqlId (identifier) {
|
|
23866
|
+
const match = identifier.match(/^`(.*)`$/);
|
|
23867
|
+
return match ? match[1] : identifier
|
|
23868
|
+
}
|
|
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
|
+
|
|
23865
23895
|
function init (parent, onSubmit, onShiftSubmit) {
|
|
23866
23896
|
addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
|
|
23867
23897
|
addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
|
|
@@ -23902,9 +23932,7 @@
|
|
|
23902
23932
|
|
|
23903
23933
|
const copyListenerFactory = (delimiter) => {
|
|
23904
23934
|
return () => {
|
|
23905
|
-
if (
|
|
23906
|
-
!window.sqlFetch?.result
|
|
23907
|
-
) {
|
|
23935
|
+
if (!window.sqlFetch?.result) {
|
|
23908
23936
|
return
|
|
23909
23937
|
}
|
|
23910
23938
|
const type = 'text/plain';
|
|
@@ -23930,6 +23958,21 @@
|
|
|
23930
23958
|
};
|
|
23931
23959
|
addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), copyListenerFactory(','));
|
|
23932
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
|
+
});
|
|
23933
23976
|
|
|
23934
23977
|
document.addEventListener('click', function (event) {
|
|
23935
23978
|
if (event.target !== dropdownButton) {
|
|
@@ -23956,16 +23999,17 @@
|
|
|
23956
23999
|
schemas.forEach(([schemaName, schema]) => {
|
|
23957
24000
|
Object.entries(schema.tables).forEach(([tableName, table]) => {
|
|
23958
24001
|
const qualifiedTableName = schemas.length === 1 ? tableName : `${schemaName}.${tableName}`;
|
|
24002
|
+
const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
|
|
23959
24003
|
const columns = Object.keys(table.columns);
|
|
23960
24004
|
editorSchema[qualifiedTableName] = columns;
|
|
23961
|
-
const alias = window.metadata.table_aliases[
|
|
24005
|
+
const alias = window.metadata.table_aliases[qualifiedTableName];
|
|
23962
24006
|
if (alias) {
|
|
23963
24007
|
editorSchema[alias] = columns;
|
|
23964
24008
|
tables.push({
|
|
23965
24009
|
label: qualifiedTableName,
|
|
23966
24010
|
detail: alias,
|
|
23967
24011
|
alias_type: 'with',
|
|
23968
|
-
quoted:
|
|
24012
|
+
quoted: `${quotedQualifiedTableName} \`${alias}\``,
|
|
23969
24013
|
unquoted: `${qualifiedTableName} ${alias}`
|
|
23970
24014
|
});
|
|
23971
24015
|
tables.push({
|
|
@@ -24018,40 +24062,81 @@
|
|
|
24018
24062
|
schema: editorSchema,
|
|
24019
24063
|
tables
|
|
24020
24064
|
};
|
|
24021
|
-
const
|
|
24065
|
+
const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
|
|
24066
|
+
const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
|
|
24067
|
+
const keywordCompletions = [];
|
|
24068
|
+
window.metadata.joins.forEach((join) => {
|
|
24069
|
+
['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
|
|
24070
|
+
keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
|
|
24071
|
+
});
|
|
24072
|
+
});
|
|
24073
|
+
let combinedKeywordCompletionSource;
|
|
24074
|
+
if (keywordCompletions.length > 0) {
|
|
24075
|
+
const customKeywordCompletionSource = ifNotIn(['QuotedIdentifier', 'SpecialVar', 'String', 'LineComment', 'BlockComment', '.'], completeFromList(keywordCompletions));
|
|
24076
|
+
combinedKeywordCompletionSource = function (context) {
|
|
24077
|
+
const original = originalKeywordCompletionSource(context);
|
|
24078
|
+
const custom = customKeywordCompletionSource(context);
|
|
24079
|
+
if (original?.options && custom?.options) {
|
|
24080
|
+
original.options = original.options.concat(custom.options);
|
|
24081
|
+
}
|
|
24082
|
+
return original
|
|
24083
|
+
};
|
|
24084
|
+
} else {
|
|
24085
|
+
combinedKeywordCompletionSource = originalKeywordCompletionSource;
|
|
24086
|
+
}
|
|
24022
24087
|
const sqlExtension = new LanguageSupport(
|
|
24023
24088
|
MySQL.language,
|
|
24024
24089
|
[
|
|
24025
24090
|
MySQL.language.data.of({
|
|
24026
24091
|
autocomplete: (context) => {
|
|
24027
|
-
const result =
|
|
24092
|
+
const result = originalSchemaCompletionSource(context);
|
|
24028
24093
|
if (!hasTableAliases || !result?.options) return result
|
|
24029
24094
|
|
|
24030
24095
|
const tree = syntaxTree(context.state);
|
|
24031
24096
|
let node = tree.resolveInner(context.pos, -1);
|
|
24097
|
+
if (!node) return result
|
|
24032
24098
|
|
|
24033
24099
|
// We are trying to identify the case where we are autocompleting a table name after "from" or "join"
|
|
24100
|
+
|
|
24034
24101
|
// TODO: we don't handle the case where a user typed "select table.foo from". In that case we probably
|
|
24035
24102
|
// shouldn't autocomplete the alias. Though, if the user typed "select table.foo, t.bar", we won't know
|
|
24036
|
-
// what to do.
|
|
24103
|
+
// what to do. Maybe it is ok to force users to simply delete the alias after autocompleting.
|
|
24104
|
+
|
|
24037
24105
|
// TODO: if table aliases aren't enabled, we don't need to override autocomplete.
|
|
24038
24106
|
|
|
24039
|
-
|
|
24107
|
+
let foundSchema;
|
|
24108
|
+
if (node.name === 'Statement') {
|
|
24040
24109
|
// The node can be a Statement if the cursor is at the end of "from " and there is a complete
|
|
24041
24110
|
// statement in the editor (semicolon present). In that case we want to find the node just before the
|
|
24042
24111
|
// current position so that we can check whether it is "from" or "join".
|
|
24043
24112
|
node = node.childBefore(context.pos);
|
|
24044
|
-
} else if (node
|
|
24113
|
+
} else if (node.name === 'Script') {
|
|
24045
24114
|
// It seems the node can sometimes be a Script if the cursor is at the end of the last statement in the
|
|
24046
24115
|
// editor and the statement doesn't end in a semicolon. In that case we can find the last statement in the
|
|
24047
24116
|
// Script so that we can check whether it is "from" or "join".
|
|
24048
24117
|
node = node.lastChild?.childBefore(context.pos);
|
|
24049
|
-
} else if (['Identifier', 'QuotedIdentifier', 'Keyword'].includes(node
|
|
24118
|
+
} else if (['Identifier', 'QuotedIdentifier', 'Keyword', '.'].includes(node.name)) {
|
|
24050
24119
|
// If the node is an Identifier, we might be in the middle of typing the table name. If the node is a
|
|
24051
24120
|
// Keyword but isn't "from" or "join", we might be in the middle of typing a table name that is similar
|
|
24052
24121
|
// to a Keyword, for instance "orders" or "selections" or "fromages". In these cases, look for the previous
|
|
24053
|
-
// sibling so that we can check whether it is "from" or "join".
|
|
24054
|
-
|
|
24122
|
+
// sibling so that we can check whether it is "from" or "join". If we found a '.' or if the previous
|
|
24123
|
+
// sibling is a '.', we might be in the middle of typing something like "schema.table" or
|
|
24124
|
+
// "`schema`.table" or "`schema`.`table`". In these cases we need to record the schema used so that we
|
|
24125
|
+
// can autocomplete table names with aliases.
|
|
24126
|
+
if (node.name !== '.') {
|
|
24127
|
+
node = node.prevSibling;
|
|
24128
|
+
}
|
|
24129
|
+
|
|
24130
|
+
if (node?.name === '.') {
|
|
24131
|
+
node = node.prevSibling;
|
|
24132
|
+
if (['Identifier', 'QuotedIdentifier'].includes(node?.name)) {
|
|
24133
|
+
foundSchema = unquoteSqlId(context.state.doc.sliceString(node.from, node.to));
|
|
24134
|
+
node = node.parent;
|
|
24135
|
+
if (node?.name === 'CompositeIdentifier') {
|
|
24136
|
+
node = node.prevSibling;
|
|
24137
|
+
}
|
|
24138
|
+
}
|
|
24139
|
+
}
|
|
24055
24140
|
}
|
|
24056
24141
|
|
|
24057
24142
|
const nodeText = node ? context.state.doc.sliceString(node.from, node.to).toLowerCase() : null;
|
|
@@ -24076,13 +24161,21 @@
|
|
|
24076
24161
|
option.apply = option.unquoted;
|
|
24077
24162
|
}
|
|
24078
24163
|
}
|
|
24164
|
+
if (foundSchema) {
|
|
24165
|
+
const unquotedLabel = unquoteSqlId(option.label);
|
|
24166
|
+
const quoted = unquotedLabel !== option.label;
|
|
24167
|
+
const alias = window.metadata.table_aliases[`${foundSchema}.${unquotedLabel}`];
|
|
24168
|
+
if (alias) {
|
|
24169
|
+
option = { label: quoted ? `\`${unquotedLabel}\` \`${alias}\`` : `${option.label} ${alias}` };
|
|
24170
|
+
}
|
|
24171
|
+
}
|
|
24079
24172
|
return option
|
|
24080
24173
|
});
|
|
24081
24174
|
return result
|
|
24082
24175
|
}
|
|
24083
24176
|
}),
|
|
24084
24177
|
MySQL.language.data.of({
|
|
24085
|
-
autocomplete:
|
|
24178
|
+
autocomplete: combinedKeywordCompletionSource
|
|
24086
24179
|
})
|
|
24087
24180
|
]
|
|
24088
24181
|
);
|
|
@@ -24168,8 +24261,8 @@
|
|
|
24168
24261
|
});
|
|
24169
24262
|
}
|
|
24170
24263
|
|
|
24171
|
-
function
|
|
24172
|
-
url.pathname = url.pathname.replace(/\/[^/]+$/, `/${
|
|
24264
|
+
function setActionInUrl (url, action) {
|
|
24265
|
+
url.pathname = url.pathname.replace(/\/[^/]+$/, `/${action}`);
|
|
24173
24266
|
}
|
|
24174
24267
|
|
|
24175
24268
|
function getTabFromUrl (url) {
|
|
@@ -24183,19 +24276,19 @@
|
|
|
24183
24276
|
|
|
24184
24277
|
function updateTabs () {
|
|
24185
24278
|
const url = new URL(window.location);
|
|
24186
|
-
|
|
24279
|
+
setActionInUrl(url, 'graph');
|
|
24187
24280
|
document.getElementById('graph-tab-button').href = url.pathname + url.search;
|
|
24188
|
-
|
|
24281
|
+
setActionInUrl(url, 'saved');
|
|
24189
24282
|
document.getElementById('saved-tab-button').href = url.pathname + url.search;
|
|
24190
|
-
|
|
24283
|
+
setActionInUrl(url, 'structure');
|
|
24191
24284
|
document.getElementById('structure-tab-button').href = url.pathname + url.search;
|
|
24192
|
-
|
|
24285
|
+
setActionInUrl(url, 'query');
|
|
24193
24286
|
document.getElementById('query-tab-button').href = url.pathname + url.search;
|
|
24194
24287
|
}
|
|
24195
24288
|
|
|
24196
24289
|
function selectTab (event, tab) {
|
|
24197
24290
|
const url = new URL(window.location);
|
|
24198
|
-
|
|
24291
|
+
setActionInUrl(url, tab);
|
|
24199
24292
|
route(event.target, event, url, true);
|
|
24200
24293
|
}
|
|
24201
24294
|
|
|
@@ -24404,6 +24497,7 @@
|
|
|
24404
24497
|
document.getElementById('graph-status').style.display = 'flex';
|
|
24405
24498
|
document.getElementById('fetch-sql-box').style.display = 'none';
|
|
24406
24499
|
document.getElementById('cancel-button').style.display = 'none';
|
|
24500
|
+
updateDownloadButtons(window?.sqlFetch);
|
|
24407
24501
|
maybeFetchResult(internal);
|
|
24408
24502
|
|
|
24409
24503
|
focus(getSelection());
|
|
@@ -24438,7 +24532,7 @@
|
|
|
24438
24532
|
setSavedStatus(`${numFiles} file${numFiles === 1 ? '' : 's'}`);
|
|
24439
24533
|
Object.values(saved).forEach(file => {
|
|
24440
24534
|
const viewUrl = new URL(window.location.origin + window.location.pathname);
|
|
24441
|
-
|
|
24535
|
+
setActionInUrl(viewUrl, 'query');
|
|
24442
24536
|
viewUrl.searchParams.set('file', file.filename);
|
|
24443
24537
|
|
|
24444
24538
|
const viewLinkElement = document.createElement('a');
|
|
@@ -24451,7 +24545,7 @@
|
|
|
24451
24545
|
});
|
|
24452
24546
|
|
|
24453
24547
|
const runUrl = new URL(window.location.origin + window.location.pathname);
|
|
24454
|
-
|
|
24548
|
+
setActionInUrl(runUrl, 'query');
|
|
24455
24549
|
runUrl.searchParams.set('file', file.filename);
|
|
24456
24550
|
runUrl.searchParams.set('run', 'true');
|
|
24457
24551
|
|
|
@@ -24552,6 +24646,7 @@
|
|
|
24552
24646
|
clearGraphStatus();
|
|
24553
24647
|
clearResultBox();
|
|
24554
24648
|
clearResultStatus();
|
|
24649
|
+
disableDownloadButtons();
|
|
24555
24650
|
}
|
|
24556
24651
|
|
|
24557
24652
|
function clearResultStatus () {
|
|
@@ -24821,7 +24916,28 @@
|
|
|
24821
24916
|
});
|
|
24822
24917
|
}
|
|
24823
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
|
+
|
|
24824
24939
|
function displaySqlFetch (fetch) {
|
|
24940
|
+
updateDownloadButtons(fetch);
|
|
24825
24941
|
if (window.tab === 'query') {
|
|
24826
24942
|
displaySqlFetchInResultTab(fetch);
|
|
24827
24943
|
} else if (window.tab === 'graph') {
|
|
@@ -24950,7 +25066,7 @@
|
|
|
24950
25066
|
if (result.total_rows === 1) {
|
|
24951
25067
|
statusElement.innerText = `${result.total_rows} row (${elapsed}s)`;
|
|
24952
25068
|
} else {
|
|
24953
|
-
statusElement.innerText = `${result.total_rows} rows (${elapsed}s)`;
|
|
25069
|
+
statusElement.innerText = `${result.total_rows.toLocaleString()} rows (${elapsed}s)`;
|
|
24954
25070
|
}
|
|
24955
25071
|
|
|
24956
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.42
|
|
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
|