sqlui 0.1.47 → 0.1.48

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: 01c105468ea4c5f463ada429231bd41a3a65752d8fa95f8befa2121a47765fbe
4
- data.tar.gz: 6e0df11aa97b0e8d0d685e6b109a297c8ddba78959f109db26f7b159aa4cfa0c
3
+ metadata.gz: ddbafc23478bb1d27023d516dda3ef412306c68e3084aa9c7e186c7372ff18ad
4
+ data.tar.gz: 5638fa4d6a7e30ecce0d89505953e2fc2d9f18e19e928c41f7c1bde4a986aba9
5
5
  SHA512:
6
- metadata.gz: 6c1fd32e0feb8817bac5e120a15e71adf48e9ccbe5183e0ba729dc29c1a2bb76ffa18bd7c63559dd49f66f0eb8a3669213988701da8c3f4d4566919985c80c13
7
- data.tar.gz: 022b592a3ed56c7e06b8222566cfe7d096ed1b1789d687afe4bf945e625a390269dffd8a701fa4647377e85399b40e345147ea637113a2489e31fbd2630e409a
6
+ metadata.gz: c99d375bf14a2f892b1a7c3e85ec576ebdc749e432e56f7e993c8437c89e8a19af96189a501b7a80e08e3b6f5743eeecd57dbfa94b056570ba1da0274d163121
7
+ data.tar.gz: 3fab12e8be52afbd23f317b16c48eed4d095f293f4bab02a9c4c515ea91d501fd027fa71b1f008831d86fa00af38fb4376ba6c3f891ba5a4c7aa7f0b39690e2c
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.47
1
+ 0.1.48
@@ -8,7 +8,7 @@ require_relative 'args'
8
8
 
9
9
  # Config for a single database.
10
10
  class DatabaseConfig
11
- attr_reader :display_name, :description, :url_path, :joins, :saved_path, :tables, :client_params
11
+ attr_reader :display_name, :description, :url_path, :joins, :saved_path, :tables, :columns, :client_params
12
12
 
13
13
  def initialize(hash)
14
14
  @display_name = Args.fetch_non_empty_string(hash, :display_name).strip
@@ -28,7 +28,7 @@ class DatabaseConfig
28
28
  raise ArgumentError, "invalid join #{join.to_json}"
29
29
  end
30
30
  @tables = Args.fetch_optional_hash(hash, :tables) || {}
31
- @tables = @tables.each do |table, table_config|
31
+ @tables.each do |table, table_config|
32
32
  unless table_config.is_a?(Hash)
33
33
  raise ArgumentError, "invalid table config for #{table} (#{table_config}), expected hash"
34
34
  end
@@ -43,6 +43,32 @@ class DatabaseConfig
43
43
  raise ArgumentError, "invalid table boost for #{table} (#{table_boost}), expected int"
44
44
  end
45
45
  end
46
+ @columns = Args.fetch_optional_hash(hash, :columns) || {}
47
+ @columns.each do |column, column_config|
48
+ unless column_config.is_a?(Hash)
49
+ raise ArgumentError, "invalid column config for #{column} (#{column_config}), expected hash"
50
+ end
51
+
52
+ links = column_config[:links]
53
+ raise ArgumentError, "invalid links for #{column} (#{links}), expected array" if links && !links.is_a?(Array)
54
+
55
+ links.each do |link_config|
56
+ unless link_config.is_a?(Hash)
57
+ raise ArgumentError, "invalid link config for #{column} (#{link_config}), expected hash"
58
+ end
59
+
60
+ unless link_config[:short_name].is_a?(String)
61
+ raise ArgumentError,
62
+ "invalid link short_name for #{column} link (#{link_config[:short_name]}), expected string"
63
+ end
64
+ unless link_config[:long_name].is_a?(String)
65
+ raise ArgumentError, "invalid link long_name for #{column} link (#{link_config[:long_name]}), expected string"
66
+ end
67
+ unless link_config[:template].is_a?(String)
68
+ raise ArgumentError, "invalid template for #{column} link (#{link_config[:template]}), expected string"
69
+ end
70
+ end
71
+ end
46
72
  aliases = @tables.map { |_table, table_config| table_config[:alias] }.compact
47
73
  if aliases.to_set.size < aliases.size
48
74
  duplicate_aliases = aliases.reject { |a| aliases.count(a) == 1 }.to_set
data/app/server.rb CHANGED
@@ -39,6 +39,10 @@ class Server < Sinatra::Base
39
39
  redirect config.list_url_path, 301
40
40
  end
41
41
 
42
+ get '/favicon.svg' do
43
+ send_file File.join(resources_dir, 'favicon.svg')
44
+ end
45
+
42
46
  get "#{config.list_url_path}/?" do
43
47
  erb :databases, locals: { config: config }
44
48
  end
@@ -69,6 +73,7 @@ class Server < Sinatra::Base
69
73
  list_url_path: config.list_url_path,
70
74
  schemas: DatabaseMetadata.lookup(client, database),
71
75
  tables: database.tables,
76
+ columns: database.columns,
72
77
  joins: database.joins,
73
78
  saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
74
79
  contents = File.read(path)
@@ -163,15 +168,18 @@ class Server < Sinatra::Base
163
168
  end
164
169
  end
165
170
 
166
- error do |e|
167
- status 500
168
- headers 'Content-Type' => 'application/json; charset=utf-8'
169
- message = e.message.lines.first&.strip || 'unexpected error'
170
- result = {
171
- error: message,
172
- stacktrace: e.backtrace.map { |b| b }.join("\n")
173
- }
174
- body result.to_json
171
+ error 400..510 do
172
+ exception = env['sinatra.error']
173
+ stacktrace = exception&.full_message(highlight: false)
174
+ if request.env['HTTP_ACCEPT'] == 'application/json'
175
+ headers 'Content-Type' => 'application/json; charset=utf-8'
176
+ message = exception&.message&.lines&.first&.strip || 'unexpected error'
177
+ json = { error: message, stacktrace: stacktrace }.compact.to_json
178
+ body json
179
+ else
180
+ message = "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
181
+ erb :error, locals: { title: "SQLUI #{message}", message: message, stacktrace: stacktrace }
182
+ end
175
183
  end
176
184
 
177
185
  run!
data/app/sql_parser.rb CHANGED
@@ -1,19 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'strscan'
4
+
3
5
  # Used to parse strings containing one or more SQL statements.
4
6
  class SqlParser
5
7
  def self.find_statement_at_cursor(sql, cursor)
6
- return sql unless sql.include?(';')
7
-
8
- parts_with_ranges = []
9
- sql.scan(/[^;]*;[ \n]*/) { |part| parts_with_ranges << [part, 0, part.size] }
10
- parts_with_ranges.inject(0) do |pos, current|
11
- current[1] += pos
12
- current[2] += pos
13
- end
8
+ parts_with_ranges = parse(sql)
14
9
  part_with_range = parts_with_ranges.find do |current|
15
- cursor >= current[1] && cursor < current[2]
10
+ cursor >= current[1] && cursor <= current[2]
16
11
  end || parts_with_ranges[-1]
17
- part_with_range[0].strip
12
+ part_with_range[0]
13
+ end
14
+
15
+ def self.parse(sql)
16
+ scanner = StringScanner.new(sql)
17
+ statements = []
18
+ single_quoted = false
19
+ double_quoted = false
20
+ single_commented = false
21
+ multi_commented = false
22
+ escaped = false
23
+ current = ''
24
+ start = 0
25
+ until scanner.eos?
26
+ current ||= ''
27
+ char = scanner.getch
28
+ current += char
29
+
30
+ if (single_quoted || double_quoted) && !escaped && char == "'" && scanner.peek(1) == "'"
31
+ current += scanner.getch
32
+ elsif escaped
33
+ escaped = false
34
+ elsif !single_quoted && !double_quoted && !escaped && !single_commented && char == '/' && scanner.peek(1) == '*'
35
+ current += scanner.getch
36
+ multi_commented = true
37
+ elsif multi_commented && char == '*' && scanner.peek(1) == '/'
38
+ multi_commented = false
39
+ elsif multi_commented
40
+ next
41
+ elsif !single_quoted && !double_quoted && !escaped && !multi_commented &&
42
+ char == '-' && scanner.peek(1).match?(/-[ \n\t]/)
43
+ current += scanner.getch
44
+ single_commented = true
45
+ elsif !single_quoted && !double_quoted && !escaped && !multi_commented && char == '#'
46
+ single_commented = true
47
+ elsif single_commented && char == "\n"
48
+ single_commented = false
49
+ elsif single_commented
50
+ single_quoted = false if char == "\n"
51
+ elsif char == '\\'
52
+ escaped = true
53
+ elsif char == "'"
54
+ single_quoted = !single_quoted unless double_quoted
55
+ elsif char == '"'
56
+ double_quoted = !double_quoted unless single_quoted
57
+ elsif char == ';' && !single_quoted && !double_quoted
58
+ # Include trailing whitespace if it runs to end of line
59
+ current += scanner.scan(/[ \t]*(?=\n)/) || ''
60
+
61
+ # Include an optional trailing single line comma
62
+ current += scanner.scan(/[ \t]*--[ \t][^\n]*/) || ''
63
+ current += scanner.scan(/[ \t]*#[^\n]*/) || ''
64
+
65
+ # Include any following blank lines, careful to avoid the final newline in case it is the start of a new query
66
+ while (more = scanner.scan(/\n[ \t]*(?=\n)/))
67
+ current += more
68
+ end
69
+
70
+ # Include remaining whitespace if that's all that's left
71
+ if scanner.rest_size == scanner.match?(/[ \t\n]*/)
72
+ current += scanner.rest
73
+ scanner.terminate
74
+ end
75
+ statements << [current, start, scanner.pos]
76
+ start = scanner.pos
77
+ current = nil
78
+ end
79
+ end
80
+ statements << [current, start, scanner.pos] unless current.nil?
81
+ statements
18
82
  end
19
83
  end
@@ -0,0 +1,21 @@
1
+ <html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= title %></title>
6
+ <link rel="icon" type="image/x-icon" href="/favicon.svg">
7
+ </head>
8
+ <body style="font-family: monospace; font-size: 16px;">
9
+
10
+ <% if defined?(message) && message %>
11
+ <p><%= message %></p>
12
+ <% end %>
13
+
14
+ <% if defined?(stacktrace) && stacktrace %>
15
+ <pre>
16
+ <%= stacktrace %>
17
+ </pre>
18
+ <% end %>
19
+
20
+ </body>
21
+ </html>
@@ -0,0 +1 @@
1
+ <svg version="1.1" viewBox="0.0 0.0 32.0 32.0" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="p.0"><path d="m0 0l32.0 0l0 32.0l-32.0 0l0 -32.0z" clip-rule="nonzero"/></clipPath><g clip-path="url(#p.0)"><path fill="#000000" fill-opacity="0.0" d="m0 0l32.0 0l0 32.0l-32.0 0z" fill-rule="evenodd"/><path fill="#6d9eeb" d="m1.3070866 20.064245l0 0c0 1.6235943 6.5782413 2.9397774 14.692913 2.9397774c8.114672 0 14.692913 -1.3161831 14.692913 -2.9397774l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774z" fill-rule="evenodd"/><path fill="#a7c4f3" d="m1.3070866 20.064245l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 20.064245l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 -8.073204" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 20.064245l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 -8.073204" fill-rule="evenodd"/><path fill="#93c47d" d="m1.3070866 12.012567l0 0c0 1.6235943 6.5782413 2.9397764 14.692913 2.9397764c8.114672 0 14.692913 -1.3161821 14.692913 -2.9397764l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755z" fill-rule="evenodd"/><path fill="#bedbb1" d="m1.3070866 12.012567l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 12.012567l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755l0 -8.073205" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 12.012567l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755l0 -8.073205" fill-rule="evenodd"/><path fill="#f4cccc" d="m1.3070866 3.862348l0 0c0 1.6235933 6.5782413 2.939776 14.692913 2.939776c8.114672 0 14.692913 -1.3161826 14.692913 -2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755z" fill-rule="evenodd"/><path fill="#f8e0e0" d="m1.3070866 3.862348l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 3.862348l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755l0 -8.073204" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 3.862348l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755l0 -8.073204" fill-rule="evenodd"/></g></svg>
@@ -158,7 +158,7 @@ p {
158
158
  border-left: 1px solid #888;
159
159
  border-right: 1px solid #888;
160
160
  border-bottom: 1px solid #888;
161
- z-index: 200;
161
+ z-index: 2;
162
162
  flex-direction: column;
163
163
  left: 0;
164
164
  right: 0;
@@ -246,6 +246,29 @@ p {
246
246
  flex-direction: column;
247
247
  }
248
248
 
249
+ #result-table tbody tr td abbr a {
250
+ color: #555;
251
+ cursor: pointer;
252
+ text-decoration: none;
253
+ margin: 0;
254
+ padding: 0 3px;
255
+ border: 1px dotted #555;
256
+ font-size: 12px;
257
+ display: inline-block;
258
+ }
259
+
260
+ #result-table tbody tr td abbr {
261
+ margin: 0 0 0 5px;
262
+ text-decoration: none;
263
+ display: inline-block;
264
+ position: relative;
265
+ top: -2px;
266
+ }
267
+
268
+ #result-table tbody tr td abbr:first-child {
269
+ margin: 0 0 0 10px;
270
+ }
271
+
249
272
  .fetch-sql-box {
250
273
  justify-content: center;
251
274
  align-items: center;
@@ -271,7 +294,7 @@ table td:last-child, table th:last-child {
271
294
  }
272
295
 
273
296
  td, th {
274
- padding: 5px 20px 5px 5px;
297
+ padding: 5px 10px;
275
298
  font-weight: normal;
276
299
  white-space: nowrap;
277
300
  max-width: 500px;
@@ -300,7 +323,7 @@ thead {
300
323
  position: -webkit-sticky;
301
324
  position: sticky;
302
325
  top: 0;
303
- z-index: 100;
326
+ z-index: 1;
304
327
  table-layout: fixed;
305
328
  }
306
329
 
@@ -2,6 +2,7 @@
2
2
  <head>
3
3
  <meta charset="utf-8">
4
4
  <title>SQLUI</title>
5
+ <link rel="icon" type="image/x-icon" href="/favicon.svg">
5
6
  <script src="sqlui.js"></script>
6
7
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
7
8
  <link rel="stylesheet" href="sqlui.css">
@@ -21666,6 +21666,57 @@
21666
21666
  }
21667
21667
  });
21668
21668
 
21669
+ function base64Encode (str) {
21670
+ // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
21671
+ return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
21672
+ function toSolidBytes (match, p1) {
21673
+ return String.fromCharCode(Number(`0x${p1}`))
21674
+ }))
21675
+ }
21676
+
21677
+ function copyTextToClipboard (text) {
21678
+ const type = 'text/plain';
21679
+ const blob = new Blob([text], { type });
21680
+ navigator.clipboard.write([new window.ClipboardItem({ [type]: blob })]);
21681
+ }
21682
+
21683
+ function toCsv (columns, rows) {
21684
+ let text = columns.map((header) => {
21685
+ if (typeof header === 'string' && (header.includes('"') || header.includes(','))) {
21686
+ return `"${header.replaceAll('"', '""')}"`
21687
+ } else {
21688
+ return header
21689
+ }
21690
+ }).join(',') + '\n';
21691
+ text += rows.map((row) => {
21692
+ return row.map((cell) => {
21693
+ if (typeof cell === 'string' && (cell.includes('"') || cell.includes(','))) {
21694
+ return `"${cell.replaceAll('"', '""')}"`
21695
+ } else {
21696
+ return cell
21697
+ }
21698
+ }).join(',')
21699
+ }).join('\n');
21700
+
21701
+ return text
21702
+ }
21703
+
21704
+ function toTsv (columns, rows) {
21705
+ if (columns.find((cell) => cell === '\t')) {
21706
+ throw new Error('TSV input may not contain a tab character.')
21707
+ }
21708
+ let text = columns.join('\t') + '\n';
21709
+
21710
+ text += rows.map((row) => {
21711
+ if (row.find((cell) => cell === '\t')) {
21712
+ throw new Error('TSV input may not contain a tab character.')
21713
+ }
21714
+ return row.join('\t')
21715
+ }).join('\n');
21716
+
21717
+ return text
21718
+ }
21719
+
21669
21720
  /// A parse stack. These are used internally by the parser to track
21670
21721
  /// parsing progress. They also provide some properties and methods
21671
21722
  /// that external code such as a tokenizer can use to get information
@@ -23860,131 +23911,7 @@
23860
23911
  builtin: MySQLBuiltin
23861
23912
  });
23862
23913
 
23863
- /* global google */
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
-
23895
- function init (parent, onSubmit, onShiftSubmit) {
23896
- addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
23897
- addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
23898
- addClickListener(document.getElementById('structure-tab-button'), (event) => selectTab(event, 'structure'));
23899
- addClickListener(document.getElementById('graph-tab-button'), (event) => selectTab(event, 'graph'));
23900
- addClickListener(document.getElementById('cancel-button'), (event) => clearResult());
23901
-
23902
- const dropdownContent = document.getElementById('submit-dropdown-content');
23903
- const dropdownButton = document.getElementById('submit-dropdown-button');
23904
- addClickListener(dropdownButton, () => dropdownContent.classList.toggle('submit-dropdown-content-show'));
23905
-
23906
- const isMac = navigator.userAgent.includes('Mac');
23907
- const runCurrentLabel = `run selection (${isMac ? '⌘' : 'Ctrl'}-Enter)`;
23908
- const runAllLabel = `run all (${isMac ? '⌘' : 'Ctrl'}-Shift-Enter)`;
23909
-
23910
- const submitButtonCurrent = document.getElementById('submit-button-current');
23911
- submitButtonCurrent.value = runCurrentLabel;
23912
- addClickListener(submitButtonCurrent, (event) => submitCurrent(event.target, event));
23913
-
23914
- const submitButtonAll = document.getElementById('submit-button-all');
23915
- submitButtonAll.value = runAllLabel;
23916
- addClickListener(submitButtonAll, (event) => submitAll(event.target, event));
23917
-
23918
- const dropdownButtonCurrent = document.getElementById('submit-dropdown-button-current');
23919
- dropdownButtonCurrent.value = runCurrentLabel;
23920
- addClickListener(dropdownButtonCurrent, (event) => submitCurrent(event.target, event));
23921
-
23922
- const dropdownAllButton = document.getElementById('submit-dropdown-button-all');
23923
- dropdownAllButton.value = runAllLabel;
23924
- addClickListener(dropdownAllButton, (event) => submitAll(event.target, event));
23925
-
23926
- const dropdownToggleButton = document.getElementById('submit-dropdown-button-toggle');
23927
- addClickListener(dropdownToggleButton, () => {
23928
- submitButtonCurrent.classList.toggle('submit-button-show');
23929
- submitButtonAll.classList.toggle('submit-button-show');
23930
- focus(getSelection());
23931
- });
23932
-
23933
- const copyListenerFactory = (delimiter) => {
23934
- return () => {
23935
- if (!window.sqlFetch?.result) {
23936
- return
23937
- }
23938
- const type = 'text/plain';
23939
- let text = window.sqlFetch.result.columns.map((header) => {
23940
- if (typeof header === 'string' && (header.includes('"') || header.includes(delimiter))) {
23941
- return `"${header.replaceAll('"', '""')}"`
23942
- } else {
23943
- return header
23944
- }
23945
- }).join(delimiter) + '\n';
23946
- text += window.sqlFetch.result.rows.map((row) => {
23947
- return row.map((cell) => {
23948
- if (typeof cell === 'string' && (cell.includes('"') || cell.includes(delimiter))) {
23949
- return `"${cell.replaceAll('"', '""')}"`
23950
- } else {
23951
- return cell
23952
- }
23953
- }).join(delimiter)
23954
- }).join('\n');
23955
- const blob = new Blob([text], { type });
23956
- navigator.clipboard.write([new window.ClipboardItem({ [type]: blob })]);
23957
- }
23958
- };
23959
- addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), copyListenerFactory(','));
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
- });
23976
-
23977
- document.addEventListener('click', function (event) {
23978
- if (event.target !== dropdownButton) {
23979
- dropdownContent.classList.remove('submit-dropdown-content-show');
23980
- }
23981
- });
23982
- dropdownContent.addEventListener('focusout', function (event) {
23983
- if (!dropdownContent.contains(event.relatedTarget)) {
23984
- dropdownContent.classList.remove('submit-dropdown-content-show');
23985
- }
23986
- });
23987
-
23914
+ function createEditor (parent, metadata, onSubmit, onShiftSubmit) {
23988
23915
  const fixedHeightEditor = EditorView.theme({
23989
23916
  '.cm-scroller': {
23990
23917
  height: '200px',
@@ -23992,7 +23919,7 @@
23992
23919
  resize: 'vertical'
23993
23920
  }
23994
23921
  });
23995
- const schemas = Object.entries(window.metadata.schemas);
23922
+ const schemas = Object.entries(metadata.schemas);
23996
23923
  const editorSchema = {};
23997
23924
  const tables = [];
23998
23925
  schemas.forEach(([schemaName, schema]) => {
@@ -24001,8 +23928,8 @@
24001
23928
  const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
24002
23929
  const columns = Object.keys(table.columns);
24003
23930
  editorSchema[qualifiedTableName] = columns;
24004
- const alias = window.metadata.tables[qualifiedTableName]?.alias;
24005
- const boost = window.metadata.tables[qualifiedTableName]?.boost;
23931
+ const alias = metadata.tables[qualifiedTableName]?.alias;
23932
+ const boost = metadata.tables[qualifiedTableName]?.boost;
24006
23933
  if (alias) {
24007
23934
  editorSchema[alias] = columns;
24008
23935
  tables.push({
@@ -24067,7 +23994,7 @@
24067
23994
  const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
24068
23995
  const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
24069
23996
  const keywordCompletions = [];
24070
- window.metadata.joins.forEach((join) => {
23997
+ metadata.joins.forEach((join) => {
24071
23998
  ['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
24072
23999
  keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
24073
24000
  });
@@ -24166,7 +24093,7 @@
24166
24093
  if (foundSchema) {
24167
24094
  const unquotedLabel = unquoteSqlId(option.label);
24168
24095
  const quoted = unquotedLabel !== option.label;
24169
- const tableConfig = window.metadata.tables[`${foundSchema}.${unquotedLabel}`];
24096
+ const tableConfig = metadata.tables[`${foundSchema}.${unquotedLabel}`];
24170
24097
  const alias = tableConfig?.alias;
24171
24098
  const boost = tableConfig?.boost || -1;
24172
24099
  const optionOverride = {
@@ -24190,7 +24117,7 @@
24190
24117
  })
24191
24118
  ]
24192
24119
  );
24193
- window.editorView = new EditorView({
24120
+ return new EditorView({
24194
24121
  state: EditorState.create({
24195
24122
  extensions: [
24196
24123
  lineNumbers(),
@@ -24217,7 +24144,149 @@
24217
24144
  ]
24218
24145
  }),
24219
24146
  parent
24147
+ })
24148
+ }
24149
+
24150
+ function unquoteSqlId (identifier) {
24151
+ const match = identifier.match(/^`(.*)`$/);
24152
+ return match ? match[1] : identifier
24153
+ }
24154
+
24155
+ function createTable (columns, rows, id, cellRenderer) {
24156
+ const tableElement = document.createElement('table');
24157
+ if (id) tableElement.id = id;
24158
+ const theadElement = document.createElement('thead');
24159
+ const headerTrElement = document.createElement('tr');
24160
+ const tbodyElement = document.createElement('tbody');
24161
+ theadElement.appendChild(headerTrElement);
24162
+ tableElement.appendChild(theadElement);
24163
+ tableElement.appendChild(tbodyElement);
24164
+
24165
+ columns.forEach(function (columnName) {
24166
+ const headerElement = document.createElement('th');
24167
+ headerElement.innerText = columnName;
24168
+ headerTrElement.appendChild(headerElement);
24169
+ });
24170
+ if (columns.length > 0) {
24171
+ headerTrElement.appendChild(document.createElement('th'));
24172
+ }
24173
+ let highlight = false;
24174
+ rows.forEach(function (row) {
24175
+ const rowElement = document.createElement('tr');
24176
+ if (highlight) {
24177
+ rowElement.classList.add('highlighted-row');
24178
+ }
24179
+ highlight = !highlight;
24180
+ tbodyElement.appendChild(rowElement);
24181
+ row.forEach(function (value, i) {
24182
+ let cellElement;
24183
+ if (cellRenderer) {
24184
+ cellElement = cellRenderer(columns[i], value);
24185
+ } else {
24186
+ cellElement = document.createElement('td');
24187
+ cellElement.innerText = value;
24188
+ }
24189
+ rowElement.appendChild(cellElement);
24190
+ });
24191
+ rowElement.appendChild(document.createElement('td'));
24192
+ });
24193
+ return tableElement
24194
+ }
24195
+
24196
+ /* global google */
24197
+
24198
+ function getSqlFromUrl (url) {
24199
+ const params = url.searchParams;
24200
+ if (params.has('file') && params.has('sql')) {
24201
+ // TODO: show an error.
24202
+ throw new Error('You can only specify a file or sql param, not both.')
24203
+ }
24204
+ if (params.has('sql')) {
24205
+ return params.get('sql')
24206
+ } else if (params.has('file')) {
24207
+ const file = params.get('file');
24208
+ const fileDetails = window.metadata.saved[file];
24209
+ if (!fileDetails) throw new Error(`no such file: ${file}`)
24210
+ return fileDetails.contents
24211
+ }
24212
+ throw new Error('You must specify a file or sql param')
24213
+ }
24214
+
24215
+ function init (parent, onSubmit, onShiftSubmit) {
24216
+ addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
24217
+ addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
24218
+ addClickListener(document.getElementById('structure-tab-button'), (event) => selectTab(event, 'structure'));
24219
+ addClickListener(document.getElementById('graph-tab-button'), (event) => selectTab(event, 'graph'));
24220
+ addClickListener(document.getElementById('cancel-button'), (event) => clearResult());
24221
+
24222
+ const dropdownContent = document.getElementById('submit-dropdown-content');
24223
+ const dropdownButton = document.getElementById('submit-dropdown-button');
24224
+ addClickListener(dropdownButton, () => dropdownContent.classList.toggle('submit-dropdown-content-show'));
24225
+
24226
+ const isMac = navigator.userAgent.includes('Mac');
24227
+ const runCurrentLabel = `run selection (${isMac ? '⌘' : 'Ctrl'}-Enter)`;
24228
+ const runAllLabel = `run all (${isMac ? '⌘' : 'Ctrl'}-Shift-Enter)`;
24229
+
24230
+ const submitButtonCurrent = document.getElementById('submit-button-current');
24231
+ submitButtonCurrent.value = runCurrentLabel;
24232
+ addClickListener(submitButtonCurrent, (event) => submitCurrent(event.target, event));
24233
+
24234
+ const submitButtonAll = document.getElementById('submit-button-all');
24235
+ submitButtonAll.value = runAllLabel;
24236
+ addClickListener(submitButtonAll, (event) => submitAll(event.target, event));
24237
+
24238
+ const dropdownButtonCurrent = document.getElementById('submit-dropdown-button-current');
24239
+ dropdownButtonCurrent.value = runCurrentLabel;
24240
+ addClickListener(dropdownButtonCurrent, (event) => submitCurrent(event.target, event));
24241
+
24242
+ const dropdownAllButton = document.getElementById('submit-dropdown-button-all');
24243
+ dropdownAllButton.value = runAllLabel;
24244
+ addClickListener(dropdownAllButton, (event) => submitAll(event.target, event));
24245
+
24246
+ const dropdownToggleButton = document.getElementById('submit-dropdown-button-toggle');
24247
+ addClickListener(dropdownToggleButton, () => {
24248
+ submitButtonCurrent.classList.toggle('submit-button-show');
24249
+ submitButtonAll.classList.toggle('submit-button-show');
24250
+ focus(getSelection());
24251
+ });
24252
+
24253
+ addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), (event) => {
24254
+ if (window.sqlFetch?.result) {
24255
+ copyTextToClipboard(toCsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
24256
+ }
24257
+ });
24258
+ addClickListener(document.getElementById('submit-dropdown-button-copy-tsv'), (event) => {
24259
+ if (window.sqlFetch?.result) {
24260
+ copyTextToClipboard(toTsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
24261
+ }
24262
+ });
24263
+ addClickListener(document.getElementById('submit-dropdown-button-download-csv'), () => {
24264
+ if (!window.sqlFetch?.result) return
24265
+
24266
+ const url = new URL(window.location);
24267
+ url.searchParams.set('sql', base64Encode(getSqlFromUrl(url)));
24268
+ url.searchParams.delete('file');
24269
+ setActionInUrl(url, 'download_csv');
24270
+
24271
+ const link = document.createElement('a');
24272
+ link.setAttribute('download', 'result.csv');
24273
+ link.setAttribute('href', url.href);
24274
+ link.click();
24275
+
24276
+ focus(getSelection());
24277
+ });
24278
+
24279
+ document.addEventListener('click', function (event) {
24280
+ if (event.target !== dropdownButton) {
24281
+ dropdownContent.classList.remove('submit-dropdown-content-show');
24282
+ }
24283
+ });
24284
+ dropdownContent.addEventListener('focusout', function (event) {
24285
+ if (!dropdownContent.contains(event.relatedTarget)) {
24286
+ dropdownContent.classList.remove('submit-dropdown-content-show');
24287
+ }
24220
24288
  });
24289
+ window.editorView = createEditor(parent, window.metadata, onSubmit, onShiftSubmit);
24221
24290
  }
24222
24291
 
24223
24292
  function addClickListener (element, func) {
@@ -24438,7 +24507,7 @@
24438
24507
  }
24439
24508
  rows.push(row);
24440
24509
  }
24441
- createTable(columnsElement, columns, rows);
24510
+ columnsElement.appendChild(createTable(columns, rows, null));
24442
24511
  }
24443
24512
 
24444
24513
  const indexEntries = Object.entries(table.indexes);
@@ -24458,49 +24527,12 @@
24458
24527
  rows.push(row);
24459
24528
  }
24460
24529
  }
24461
- createTable(indexesElement, columns, rows);
24530
+ indexesElement.appendChild(createTable(columns, rows, null));
24462
24531
  }
24463
24532
  });
24464
24533
  window.structureLoaded = true;
24465
24534
  }
24466
24535
 
24467
- function createTable (parent, columns, rows) {
24468
- const tableElement = document.createElement('table');
24469
- const theadElement = document.createElement('thead');
24470
- const headerTrElement = document.createElement('tr');
24471
- const tbodyElement = document.createElement('tbody');
24472
- theadElement.appendChild(headerTrElement);
24473
- tableElement.appendChild(theadElement);
24474
- tableElement.appendChild(tbodyElement);
24475
- parent.appendChild(tableElement);
24476
-
24477
- columns.forEach(function (columnName) {
24478
- const headerElement = document.createElement('th');
24479
- headerElement.classList.add('cell');
24480
- headerElement.innerText = columnName;
24481
- headerTrElement.appendChild(headerElement);
24482
- });
24483
- if (columns.length > 0) {
24484
- headerTrElement.appendChild(document.createElement('th'));
24485
- }
24486
- let highlight = false;
24487
- rows.forEach(function (row) {
24488
- const rowElement = document.createElement('tr');
24489
- if (highlight) {
24490
- rowElement.classList.add('highlighted-row');
24491
- }
24492
- highlight = !highlight;
24493
- tbodyElement.appendChild(rowElement);
24494
- row.forEach(function (value) {
24495
- const cellElement = document.createElement('td');
24496
- cellElement.classList.add('cell');
24497
- cellElement.innerText = value;
24498
- rowElement.appendChild(cellElement);
24499
- });
24500
- rowElement.appendChild(document.createElement('td'));
24501
- });
24502
- }
24503
-
24504
24536
  function selectGraphTab (internal) {
24505
24537
  document.getElementById('query-box').style.display = 'flex';
24506
24538
  document.getElementById('submit-box').style.display = 'flex';
@@ -24892,39 +24924,32 @@
24892
24924
  clearResultBox();
24893
24925
  displaySqlFetchResultStatus('result-status', fetch);
24894
24926
 
24895
- const tableElement = document.createElement('table');
24896
- tableElement.id = 'result-table';
24897
- const theadElement = document.createElement('thead');
24898
- const headerElement = document.createElement('tr');
24899
- const tbodyElement = document.createElement('tbody');
24900
- theadElement.appendChild(headerElement);
24901
- tableElement.appendChild(theadElement);
24902
- tableElement.appendChild(tbodyElement);
24903
- document.getElementById('result-box').appendChild(tableElement);
24927
+ const createLink = function (link, value) {
24928
+ const linkElement = document.createElement('a');
24929
+ linkElement.href = link.template.replaceAll('{*}', encodeURIComponent(value));
24930
+ linkElement.innerText = link.short_name;
24931
+ linkElement.target = '_blank';
24904
24932
 
24905
- fetch.result.columns.forEach(column => {
24906
- const template = document.createElement('template');
24907
- template.innerHTML = `<th class="cell">${column}</th>`;
24908
- headerElement.appendChild(template.content.firstChild);
24909
- });
24910
- if (fetch.result.columns.length > 0) {
24911
- headerElement.appendChild(document.createElement('th'));
24912
- }
24913
- let highlight = false;
24914
- fetch.result.rows.forEach(function (row) {
24915
- const rowElement = document.createElement('tr');
24916
- if (highlight) {
24917
- rowElement.classList.add('highlighted-row');
24933
+ const abbrElement = document.createElement('abbr');
24934
+ abbrElement.title = link.long_name;
24935
+ abbrElement.appendChild(linkElement);
24936
+
24937
+ return abbrElement
24938
+ };
24939
+ const cellRenderer = function (column, value) {
24940
+ const cellElement = document.createElement('td');
24941
+ if (window.metadata.columns[column]?.links?.length > 0) {
24942
+ cellElement.appendChild(document.createTextNode(value));
24943
+ window.metadata.columns[column].links.forEach((link) => {
24944
+ cellElement.appendChild(createLink(link, value));
24945
+ });
24946
+ } else {
24947
+ cellElement.innerText = value;
24918
24948
  }
24919
- highlight = !highlight;
24920
- tbodyElement.appendChild(rowElement);
24921
- row.forEach(function (value) {
24922
- const template = document.createElement('template');
24923
- template.innerHTML = `<td class="cell">${value}</td>`;
24924
- rowElement.appendChild(template.content.firstChild);
24925
- });
24926
- rowElement.appendChild(document.createElement('td'));
24927
- });
24949
+ return cellElement
24950
+ };
24951
+ document.getElementById('result-box')
24952
+ .appendChild(createTable(fetch.result.columns, fetch.result.rows, 'result-table', cellRenderer));
24928
24953
  }
24929
24954
 
24930
24955
  function disableDownloadButtons () {
@@ -25116,10 +25141,12 @@
25116
25141
  if (contentType && contentType.indexOf('application/json') !== -1) {
25117
25142
  return response.json().then((result) => {
25118
25143
  if (result.error) {
25119
- let error = `<pre>${result.error}`;
25144
+ let error = '<div style="font-family: monospace; font-size: 16px;">\n';
25145
+ error += `<div>${result.error}</div>\n`;
25120
25146
  if (result.stacktrace) {
25121
- error += '\n' + result.stacktrace + '</pre>';
25147
+ error += '<pre>\n' + result.stacktrace + '\n</pre>\n';
25122
25148
  }
25149
+ error += '</div>\n';
25123
25150
  document.getElementById('loading-box').innerHTML = error;
25124
25151
  } else if (!result.server) {
25125
25152
  document.getElementById('loading-box').innerHTML = `
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.47
4
+ version: 0.1.48
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-21 00:00:00.000000000 Z
11
+ date: 2022-11-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2
@@ -140,7 +140,9 @@ files:
140
140
  - app/sqlui.rb
141
141
  - app/sqlui_config.rb
142
142
  - app/views/databases.erb
143
+ - app/views/error.erb
143
144
  - bin/sqlui
145
+ - client/resources/favicon.svg
144
146
  - client/resources/sqlui.css
145
147
  - client/resources/sqlui.html
146
148
  - client/resources/sqlui.js