sqlui 0.1.46 → 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: 5a850c634ce4336368499706bfec9096f7a478a26411888a9d0017a52c83c2aa
4
- data.tar.gz: cd08928c55c5a1f200cd275f51b8bf946744094bd1e522029e751d6458f91bf9
3
+ metadata.gz: ddbafc23478bb1d27023d516dda3ef412306c68e3084aa9c7e186c7372ff18ad
4
+ data.tar.gz: 5638fa4d6a7e30ecce0d89505953e2fc2d9f18e19e928c41f7c1bde4a986aba9
5
5
  SHA512:
6
- metadata.gz: d029bef28fa07f45c51088e38e8e5dff6fc8cb5e9a23f8417504026a42c9b64b4b19e5d60c1f596c160c556772eae931baa249d28bab73c09b99e94e1fd0481c
7
- data.tar.gz: 217e338065a53a2d266222b92b4ca3d2ca02fdf6cdd741ff6b7010dd558f9caef8b1bf8856be3b04a36de3ec08bd1d2d01af5a575b3c07ae7ba7be30d715f983
6
+ metadata.gz: c99d375bf14a2f892b1a7c3e85ec576ebdc749e432e56f7e993c8437c89e8a19af96189a501b7a80e08e3b6f5743eeecd57dbfa94b056570ba1da0274d163121
7
+ data.tar.gz: 3fab12e8be52afbd23f317b16c48eed4d095f293f4bab02a9c4c515ea91d501fd027fa71b1f008831d86fa00af38fb4376ba6c3f891ba5a4c7aa7f0b39690e2c
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.46
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, :table_aliases, :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
@@ -27,12 +27,51 @@ class DatabaseConfig
27
27
 
28
28
  raise ArgumentError, "invalid join #{join.to_json}"
29
29
  end
30
- @table_aliases = Args.fetch_optional_hash(hash, :table_aliases) || {}
31
- @table_aliases = @table_aliases.each do |table, a|
32
- raise ArgumentError, "invalid alias for table #{table} (#{a}), expected string" unless a.is_a?(String)
30
+ @tables = Args.fetch_optional_hash(hash, :tables) || {}
31
+ @tables.each do |table, table_config|
32
+ unless table_config.is_a?(Hash)
33
+ raise ArgumentError, "invalid table config for #{table} (#{table_config}), expected hash"
34
+ end
35
+
36
+ table_alias = table_config[:alias]
37
+ if table_alias && !table_alias.is_a?(String)
38
+ raise ArgumentError, "invalid table alias for #{table} (#{table_alias}), expected string"
39
+ end
40
+
41
+ table_boost = table_config[:boost]
42
+ if table_boost && !table_boost.is_a?(Integer)
43
+ raise ArgumentError, "invalid table boost for #{table} (#{table_boost}), expected int"
44
+ end
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
33
71
  end
34
- duplicate_aliases = @table_aliases.reject { |(_, v)| @table_aliases.values.count(v) == 1 }.to_h.values.to_set
35
- if @table_aliases.values.to_set.size < @table_aliases.values.size
72
+ aliases = @tables.map { |_table, table_config| table_config[:alias] }.compact
73
+ if aliases.to_set.size < aliases.size
74
+ duplicate_aliases = aliases.reject { |a| aliases.count(a) == 1 }.to_set
36
75
  raise ArgumentError, "duplicate table aliases: #{duplicate_aliases.join(', ')}"
37
76
  end
38
77
 
data/app/server.rb CHANGED
@@ -6,7 +6,6 @@ require 'erb'
6
6
  require 'json'
7
7
  require 'sinatra/base'
8
8
  require 'uri'
9
- require 'webrick/log'
10
9
  require_relative 'database_metadata'
11
10
  require_relative 'mysql_types'
12
11
  require_relative 'sql_parser'
@@ -14,6 +13,10 @@ require_relative 'sqlui'
14
13
 
15
14
  # SQLUI Sinatra server.
16
15
  class Server < Sinatra::Base
16
+ def self.logger
17
+ @logger ||= WEBrick::Log.new
18
+ end
19
+
17
20
  def self.init_and_run(config, resources_dir)
18
21
  Mysql2::Client.default_query_options[:as] = :array
19
22
  Mysql2::Client.default_query_options[:cast_booleans] = true
@@ -27,8 +30,6 @@ class Server < Sinatra::Base
27
30
  set :raise_errors, false
28
31
  set :show_exceptions, false
29
32
 
30
- logger = WEBrick::Log.new
31
-
32
33
  get '/-/health' do
33
34
  status 200
34
35
  body 'OK'
@@ -38,6 +39,10 @@ class Server < Sinatra::Base
38
39
  redirect config.list_url_path, 301
39
40
  end
40
41
 
42
+ get '/favicon.svg' do
43
+ send_file File.join(resources_dir, 'favicon.svg')
44
+ end
45
+
41
46
  get "#{config.list_url_path}/?" do
42
47
  erb :databases, locals: { config: config }
43
48
  end
@@ -67,7 +72,8 @@ class Server < Sinatra::Base
67
72
  server: "#{config.name} - #{database.display_name}",
68
73
  list_url_path: config.list_url_path,
69
74
  schemas: DatabaseMetadata.lookup(client, database),
70
- table_aliases: database.table_aliases,
75
+ tables: database.tables,
76
+ columns: database.columns,
71
77
  joins: database.joins,
72
78
  saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
73
79
  contents = File.read(path)
@@ -99,35 +105,45 @@ class Server < Sinatra::Base
99
105
  variables = params[:variables] || {}
100
106
  sql = find_selected_query(params[:sql], params[:selection])
101
107
 
102
- result = database.with_client do |client|
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
- }
113
- end
114
-
115
- result[:selection] = params[:selection]
116
- result[:query] = params[:sql]
117
-
118
108
  status 200
119
109
  headers 'Content-Type' => 'application/json; charset=utf-8'
120
- body result.to_json
110
+
111
+ database.with_client do |client|
112
+ query_result = execute_query(client, variables, sql)
113
+ stream do |out|
114
+ json = <<~RES.chomp
115
+ {
116
+ "columns": #{query_result.fields.to_json},
117
+ "column_types": #{MysqlTypes.map_to_google_charts_types(query_result.field_types).to_json},
118
+ "total_rows": #{query_result.size.to_json},
119
+ "selection": #{params[:selection].to_json},
120
+ "query": #{params[:sql].to_json},
121
+ "rows": [
122
+ RES
123
+ out << json
124
+ bytes = json.bytesize
125
+ query_result.each_with_index do |row, i|
126
+ json = "#{i.zero? ? '' : ','}\n #{row.to_json}"
127
+ bytes += json.bytesize
128
+ break if i == Sqlui::MAX_ROWS || bytes > Sqlui::MAX_BYTES
129
+
130
+ out << json
131
+ end
132
+ out << <<~RES
133
+
134
+ ]
135
+ }
136
+ RES
137
+ end
138
+ end
121
139
  end
122
140
 
123
141
  get "#{database.url_path}/download_csv" do
124
142
  break client_error('missing sql') unless params[:sql]
125
143
 
126
144
  sql = Base64.decode64(params[:sql]).force_encoding('UTF-8')
127
- logger.info "sql: #{sql}"
128
145
  variables = params.map { |k, v| k[0] == '_' ? [k, v] : nil }.compact.to_h
129
146
  sql = find_selected_query(sql, params[:selection])
130
- logger.info "sql: #{sql}"
131
147
 
132
148
  content_type 'application/csv; charset=utf-8'
133
149
  attachment 'result.csv'
@@ -152,15 +168,18 @@ class Server < Sinatra::Base
152
168
  end
153
169
  end
154
170
 
155
- error do |e|
156
- status 500
157
- headers 'Content-Type' => 'application/json; charset=utf-8'
158
- message = e.message.lines.first&.strip || 'unexpected error'
159
- result = {
160
- error: message,
161
- stacktrace: e.backtrace.map { |b| b }.join("\n")
162
- }
163
- 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
164
183
  end
165
184
 
166
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
data/app/sqlui.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'server'
6
6
  # Main entry point.
7
7
  class Sqlui
8
8
  MAX_ROWS = 10_000
9
+ MAX_BYTES = 10 * 1_024 * 1_024
9
10
 
10
11
  def initialize(config_file)
11
12
  raise 'you must specify a configuration file' unless config_file
@@ -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,22 +23919,23 @@
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
- const hasTableAliases = Object.keys(window.metadata.table_aliases).length > 0;
23999
23925
  schemas.forEach(([schemaName, schema]) => {
24000
23926
  Object.entries(schema.tables).forEach(([tableName, table]) => {
24001
23927
  const qualifiedTableName = schemas.length === 1 ? tableName : `${schemaName}.${tableName}`;
24002
23928
  const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
24003
23929
  const columns = Object.keys(table.columns);
24004
23930
  editorSchema[qualifiedTableName] = columns;
24005
- const alias = window.metadata.table_aliases[qualifiedTableName];
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({
24009
23936
  label: qualifiedTableName,
24010
23937
  detail: alias,
23938
+ boost,
24011
23939
  alias_type: 'with',
24012
23940
  quoted: `${quotedQualifiedTableName} \`${alias}\``,
24013
23941
  unquoted: `${qualifiedTableName} ${alias}`
@@ -24015,6 +23943,7 @@
24015
23943
  tables.push({
24016
23944
  label: qualifiedTableName,
24017
23945
  detail: alias,
23946
+ boost,
24018
23947
  alias_type: 'only',
24019
23948
  quoted: '`' + alias + '`',
24020
23949
  unquoted: alias
@@ -24065,7 +23994,7 @@
24065
23994
  const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
24066
23995
  const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
24067
23996
  const keywordCompletions = [];
24068
- window.metadata.joins.forEach((join) => {
23997
+ metadata.joins.forEach((join) => {
24069
23998
  ['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
24070
23999
  keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
24071
24000
  });
@@ -24090,7 +24019,7 @@
24090
24019
  MySQL.language.data.of({
24091
24020
  autocomplete: (context) => {
24092
24021
  const result = originalSchemaCompletionSource(context);
24093
- if (!hasTableAliases || !result?.options) return result
24022
+ if (!result?.options) return result
24094
24023
 
24095
24024
  const tree = syntaxTree(context.state);
24096
24025
  let node = tree.resolveInner(context.pos, -1);
@@ -24164,10 +24093,19 @@
24164
24093
  if (foundSchema) {
24165
24094
  const unquotedLabel = unquoteSqlId(option.label);
24166
24095
  const quoted = unquotedLabel !== option.label;
24167
- const alias = window.metadata.table_aliases[`${foundSchema}.${unquotedLabel}`];
24096
+ const tableConfig = metadata.tables[`${foundSchema}.${unquotedLabel}`];
24097
+ const alias = tableConfig?.alias;
24098
+ const boost = tableConfig?.boost || -1;
24099
+ const optionOverride = {
24100
+ label: option.label
24101
+ };
24168
24102
  if (alias) {
24169
- option = { label: quoted ? `\`${unquotedLabel}\` \`${alias}\`` : `${option.label} ${alias}` };
24103
+ optionOverride.label = quoted ? `\`${unquotedLabel}\` \`${alias}\`` : `${option.label} ${alias}`;
24170
24104
  }
24105
+ if (boost) {
24106
+ optionOverride.boost = boost;
24107
+ }
24108
+ if (alias || boost) return optionOverride
24171
24109
  }
24172
24110
  return option
24173
24111
  });
@@ -24179,7 +24117,7 @@
24179
24117
  })
24180
24118
  ]
24181
24119
  );
24182
- window.editorView = new EditorView({
24120
+ return new EditorView({
24183
24121
  state: EditorState.create({
24184
24122
  extensions: [
24185
24123
  lineNumbers(),
@@ -24206,7 +24144,149 @@
24206
24144
  ]
24207
24145
  }),
24208
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);
24209
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
+ }
24288
+ });
24289
+ window.editorView = createEditor(parent, window.metadata, onSubmit, onShiftSubmit);
24210
24290
  }
24211
24291
 
24212
24292
  function addClickListener (element, func) {
@@ -24427,7 +24507,7 @@
24427
24507
  }
24428
24508
  rows.push(row);
24429
24509
  }
24430
- createTable(columnsElement, columns, rows);
24510
+ columnsElement.appendChild(createTable(columns, rows, null));
24431
24511
  }
24432
24512
 
24433
24513
  const indexEntries = Object.entries(table.indexes);
@@ -24447,49 +24527,12 @@
24447
24527
  rows.push(row);
24448
24528
  }
24449
24529
  }
24450
- createTable(indexesElement, columns, rows);
24530
+ indexesElement.appendChild(createTable(columns, rows, null));
24451
24531
  }
24452
24532
  });
24453
24533
  window.structureLoaded = true;
24454
24534
  }
24455
24535
 
24456
- function createTable (parent, columns, rows) {
24457
- const tableElement = document.createElement('table');
24458
- const theadElement = document.createElement('thead');
24459
- const headerTrElement = document.createElement('tr');
24460
- const tbodyElement = document.createElement('tbody');
24461
- theadElement.appendChild(headerTrElement);
24462
- tableElement.appendChild(theadElement);
24463
- tableElement.appendChild(tbodyElement);
24464
- parent.appendChild(tableElement);
24465
-
24466
- columns.forEach(function (columnName) {
24467
- const headerElement = document.createElement('th');
24468
- headerElement.classList.add('cell');
24469
- headerElement.innerText = columnName;
24470
- headerTrElement.appendChild(headerElement);
24471
- });
24472
- if (columns.length > 0) {
24473
- headerTrElement.appendChild(document.createElement('th'));
24474
- }
24475
- let highlight = false;
24476
- rows.forEach(function (row) {
24477
- const rowElement = document.createElement('tr');
24478
- if (highlight) {
24479
- rowElement.classList.add('highlighted-row');
24480
- }
24481
- highlight = !highlight;
24482
- tbodyElement.appendChild(rowElement);
24483
- row.forEach(function (value) {
24484
- const cellElement = document.createElement('td');
24485
- cellElement.classList.add('cell');
24486
- cellElement.innerText = value;
24487
- rowElement.appendChild(cellElement);
24488
- });
24489
- rowElement.appendChild(document.createElement('td'));
24490
- });
24491
- }
24492
-
24493
24536
  function selectGraphTab (internal) {
24494
24537
  document.getElementById('query-box').style.display = 'flex';
24495
24538
  document.getElementById('submit-box').style.display = 'flex';
@@ -24881,39 +24924,32 @@
24881
24924
  clearResultBox();
24882
24925
  displaySqlFetchResultStatus('result-status', fetch);
24883
24926
 
24884
- const tableElement = document.createElement('table');
24885
- tableElement.id = 'result-table';
24886
- const theadElement = document.createElement('thead');
24887
- const headerElement = document.createElement('tr');
24888
- const tbodyElement = document.createElement('tbody');
24889
- theadElement.appendChild(headerElement);
24890
- tableElement.appendChild(theadElement);
24891
- tableElement.appendChild(tbodyElement);
24892
- 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';
24893
24932
 
24894
- fetch.result.columns.forEach(column => {
24895
- const template = document.createElement('template');
24896
- template.innerHTML = `<th class="cell">${column}</th>`;
24897
- headerElement.appendChild(template.content.firstChild);
24898
- });
24899
- if (fetch.result.columns.length > 0) {
24900
- headerElement.appendChild(document.createElement('th'));
24901
- }
24902
- let highlight = false;
24903
- fetch.result.rows.forEach(function (row) {
24904
- const rowElement = document.createElement('tr');
24905
- if (highlight) {
24906
- 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;
24907
24948
  }
24908
- highlight = !highlight;
24909
- tbodyElement.appendChild(rowElement);
24910
- row.forEach(function (value) {
24911
- const template = document.createElement('template');
24912
- template.innerHTML = `<td class="cell">${value}</td>`;
24913
- rowElement.appendChild(template.content.firstChild);
24914
- });
24915
- rowElement.appendChild(document.createElement('td'));
24916
- });
24949
+ return cellElement
24950
+ };
24951
+ document.getElementById('result-box')
24952
+ .appendChild(createTable(fetch.result.columns, fetch.result.rows, 'result-table', cellRenderer));
24917
24953
  }
24918
24954
 
24919
24955
  function disableDownloadButtons () {
@@ -25105,10 +25141,12 @@
25105
25141
  if (contentType && contentType.indexOf('application/json') !== -1) {
25106
25142
  return response.json().then((result) => {
25107
25143
  if (result.error) {
25108
- let error = `<pre>${result.error}`;
25144
+ let error = '<div style="font-family: monospace; font-size: 16px;">\n';
25145
+ error += `<div>${result.error}</div>\n`;
25109
25146
  if (result.stacktrace) {
25110
- error += '\n' + result.stacktrace + '</pre>';
25147
+ error += '<pre>\n' + result.stacktrace + '\n</pre>\n';
25111
25148
  }
25149
+ error += '</div>\n';
25112
25150
  document.getElementById('loading-box').innerHTML = error;
25113
25151
  } else if (!result.server) {
25114
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.46
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-20 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