sqlui 0.1.19 → 0.1.20
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 +33 -0
- data/app/database_config.rb +7 -7
- data/app/database_metadata.rb +116 -0
- data/app/deep.rb +58 -0
- data/app/mysql_types.rb +33 -0
- data/app/server.rb +124 -218
- data/app/sqlui.rb +36 -0
- data/app/sqlui_config.rb +11 -16
- data/bin/sqlui +3 -1
- data/client/resources/sqlui.css +263 -242
- data/client/resources/sqlui.html +8 -5
- data/client/resources/sqlui.js +166 -81
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f41becc73029aa0f4e09476edbe96d273ed59b6dfd778eed97728c9d8b957e8e
|
4
|
+
data.tar.gz: 845897a0a4ab1a39e2ee2e6b8e5688b2548c0f30a463ef48171bd4953c138c01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85a81eeb433be3d01e0ecbacc990515249cc620c50cb852639a15a35849d2a69461c5c975140c1cb3ba743ac5f719756c0d1c7c44eed6e869f368cd32fd7386c
|
7
|
+
data.tar.gz: 9ad8b11e30e9ef47689eb01a7eb961fe409a7a5b10b06f32cdab6ce16a1ac43adaf91ab5a2643f51095d50262309f67d1af09cf8b304d532668380105c65c1dc
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.20
|
data/app/args.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Argument validation.
|
4
|
+
class Args
|
5
|
+
def self.fetch_non_empty_string(hash, key)
|
6
|
+
value = fetch_non_nil(hash, key, String)
|
7
|
+
raise ArgumentError, "required parameter #{key} empty" if value.strip.empty?
|
8
|
+
|
9
|
+
value
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.fetch_non_empty_hash(hash, key)
|
13
|
+
value = fetch_non_nil(hash, key, Hash)
|
14
|
+
raise ArgumentError, "required parameter #{key} empty" if value.empty?
|
15
|
+
|
16
|
+
value
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.fetch_non_nil(hash, key, *classes)
|
20
|
+
raise ArgumentError, "required parameter #{key} missing" unless hash.key?(key)
|
21
|
+
|
22
|
+
value = hash[key]
|
23
|
+
raise ArgumentError, "required parameter #{key} null" if value.nil?
|
24
|
+
|
25
|
+
if classes.size.positive? && !classes.find { |clazz| value.is_a?(clazz) }
|
26
|
+
raise ArgumentError, "required parameter #{key} not a #{classes[0].to_s.downcase}" if classes.size == 1
|
27
|
+
|
28
|
+
raise ArgumentError, "required parameter #{key} not #{classes.map(&:to_s).map(&:downcase).join(' or ')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
value
|
32
|
+
end
|
33
|
+
end
|
data/app/database_config.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'mysql2'
|
4
|
+
require_relative 'args'
|
4
5
|
|
5
6
|
# Config for a single database.
|
6
7
|
class DatabaseConfig
|
7
|
-
attr_reader :display_name, :description, :url_path, :saved_path, :client_params
|
8
|
+
attr_reader :display_name, :description, :url_path, :saved_path, :client_params
|
8
9
|
|
9
10
|
def initialize(hash)
|
10
|
-
@display_name = hash
|
11
|
-
@description = hash
|
12
|
-
@url_path = hash
|
11
|
+
@display_name = Args.fetch_non_empty_string(hash, :display_name).strip
|
12
|
+
@description = Args.fetch_non_empty_string(hash, :description).strip
|
13
|
+
@url_path = Args.fetch_non_empty_string(hash, :url_path).strip
|
13
14
|
raise ArgumentError, 'url_path should start with a /' unless @url_path.start_with?('/')
|
14
15
|
raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
|
15
16
|
|
16
|
-
@saved_path = hash
|
17
|
-
@client_params = hash
|
18
|
-
@database = @client_params[:database].strip
|
17
|
+
@saved_path = Args.fetch_non_empty_string(hash, :saved_path).strip
|
18
|
+
@client_params = Args.fetch_non_empty_hash(hash, :client_params)
|
19
19
|
end
|
20
20
|
|
21
21
|
def with_client(&block)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Loads metadata from a database.
|
4
|
+
class DatabaseMetadata
|
5
|
+
def initialize
|
6
|
+
raise "can't instantiate #{self.class}"
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def lookup(client, database_config)
|
11
|
+
result = load_columns(client, database_config)
|
12
|
+
load_stats(result, client, database_config)
|
13
|
+
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def load_columns(client, database_config)
|
20
|
+
result = {}
|
21
|
+
database = database_config.client_params[:database]
|
22
|
+
where_clause = if database
|
23
|
+
"where table_schema = '#{database}'"
|
24
|
+
else
|
25
|
+
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
26
|
+
end
|
27
|
+
column_result = client.query(
|
28
|
+
<<~SQL
|
29
|
+
select
|
30
|
+
table_schema,
|
31
|
+
table_name,
|
32
|
+
column_name,
|
33
|
+
data_type,
|
34
|
+
character_maximum_length,
|
35
|
+
is_nullable,
|
36
|
+
column_key,
|
37
|
+
column_default,
|
38
|
+
extra
|
39
|
+
from information_schema.columns
|
40
|
+
#{where_clause}
|
41
|
+
order by table_schema, table_name, column_name, ordinal_position;
|
42
|
+
SQL
|
43
|
+
)
|
44
|
+
column_result.each do |row|
|
45
|
+
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
46
|
+
table_schema = row[:table_schema]
|
47
|
+
unless result[table_schema]
|
48
|
+
result[table_schema] = {
|
49
|
+
tables: {}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
table_name = row[:table_name]
|
53
|
+
tables = result[table_schema][:tables]
|
54
|
+
unless tables[table_name]
|
55
|
+
tables[table_name] = {
|
56
|
+
indexes: {},
|
57
|
+
columns: {}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
columns = result[table_schema][:tables][table_name][:columns]
|
61
|
+
column_name = row[:column_name]
|
62
|
+
columns[column_name] = {} unless columns[column_name]
|
63
|
+
column = columns[column_name]
|
64
|
+
column[:name] = column_name
|
65
|
+
column[:data_type] = row[:data_type]
|
66
|
+
column[:length] = row[:character_maximum_length]
|
67
|
+
column[:allow_null] = row[:is_nullable]
|
68
|
+
column[:key] = row[:column_key]
|
69
|
+
column[:default] = row[:column_default]
|
70
|
+
column[:extra] = row[:extra]
|
71
|
+
end
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_stats(result, client, database_config)
|
76
|
+
database = database_config.client_params[:database]
|
77
|
+
|
78
|
+
where_clause = if database
|
79
|
+
"where table_schema = '#{database}'"
|
80
|
+
else
|
81
|
+
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
82
|
+
end
|
83
|
+
stats_result = client.query(
|
84
|
+
<<~SQL
|
85
|
+
select
|
86
|
+
table_schema,
|
87
|
+
table_name,
|
88
|
+
index_name,
|
89
|
+
seq_in_index,
|
90
|
+
non_unique,
|
91
|
+
column_name
|
92
|
+
from information_schema.statistics
|
93
|
+
#{where_clause}
|
94
|
+
order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
|
95
|
+
SQL
|
96
|
+
)
|
97
|
+
stats_result.each do |row|
|
98
|
+
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
99
|
+
table_schema = row[:table_schema]
|
100
|
+
tables = result[table_schema][:tables]
|
101
|
+
table_name = row[:table_name]
|
102
|
+
indexes = tables[table_name][:indexes]
|
103
|
+
index_name = row[:index_name]
|
104
|
+
indexes[index_name] = {} unless indexes[index_name]
|
105
|
+
index = indexes[index_name]
|
106
|
+
column_name = row[:column_name]
|
107
|
+
index[column_name] = {}
|
108
|
+
column = index[column_name]
|
109
|
+
column[:name] = index_name
|
110
|
+
column[:seq_in_index] = row[:seq_in_index]
|
111
|
+
column[:non_unique] = row[:non_unique]
|
112
|
+
column[:column_name] = row[:column_name]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/app/deep.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Deep extensions for Enumerable.
|
4
|
+
module Enumerable
|
5
|
+
def deep_transform_keys!(&block)
|
6
|
+
each { |value| value.deep_transform_keys!(&block) if value.respond_to?(:deep_transform_keys!) }
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
def deep_dup(result = {})
|
11
|
+
map do |value|
|
12
|
+
value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Deep extensions for Hash.
|
19
|
+
class Hash
|
20
|
+
def deep_transform_keys!(&block)
|
21
|
+
transform_keys!(&:to_s)
|
22
|
+
each_value { |value| value.deep_transform_keys!(&block) if value.respond_to?(:deep_transform_keys!) }
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def deep_dup(result = {})
|
27
|
+
each do |key, value|
|
28
|
+
result[key] = value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def deep_set(*path, value:)
|
34
|
+
raise ArgumentError, 'no path specified' if path.empty?
|
35
|
+
|
36
|
+
if path.size == 1
|
37
|
+
self[path[0]] = value
|
38
|
+
else
|
39
|
+
raise KeyError, "key not found: #{path[0]}" unless key?(path[0])
|
40
|
+
raise ArgumentError, "value for key is not a hash: #{path[0]}" unless self.[](path[0]).is_a?(Hash)
|
41
|
+
|
42
|
+
self.[](path[0]).deep_set(*path[1..], value: value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def deep_delete(*path)
|
47
|
+
raise ArgumentError, 'no path specified' if path.empty?
|
48
|
+
raise KeyError, "key not found: #{path[0]}" unless key?(path[0])
|
49
|
+
|
50
|
+
if path.size == 1
|
51
|
+
delete(path[0])
|
52
|
+
else
|
53
|
+
raise ArgumentError, "value for key is not a hash: #{path[0]}" unless self.[](path[0]).is_a?(Hash)
|
54
|
+
|
55
|
+
self.[](path[0]).deep_delete(*path[1..])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/app/mysql_types.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Maps MySQL data types to type descriptions the client can use.
|
4
|
+
class MysqlTypes
|
5
|
+
def initialize
|
6
|
+
raise "can't instantiate #{self.class}"
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def map_to_google_charts_types(types)
|
11
|
+
types.map do |type|
|
12
|
+
type.gsub!(/\([^)]*\)/, '').strip!
|
13
|
+
case type
|
14
|
+
when 'bool', 'boolean'
|
15
|
+
'boolean'
|
16
|
+
when /double/, 'bigint', 'bit', 'dec', 'decimal', 'float', 'int', 'integer', 'mediumint', 'smallint', 'tinyint'
|
17
|
+
'number'
|
18
|
+
when /char/, /binary/, /blob/, /text/, 'enum', 'set', /image/
|
19
|
+
'string'
|
20
|
+
when 'date', 'year'
|
21
|
+
'date'
|
22
|
+
when 'datetime', 'timestamp'
|
23
|
+
'datetime'
|
24
|
+
when 'time'
|
25
|
+
'timeofday'
|
26
|
+
else
|
27
|
+
puts "unexpected MySQL type: #{type}"
|
28
|
+
'string'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/app/server.rb
CHANGED
@@ -4,112 +4,136 @@ require 'erb'
|
|
4
4
|
require 'json'
|
5
5
|
require 'sinatra/base'
|
6
6
|
require 'uri'
|
7
|
+
require_relative 'database_metadata'
|
7
8
|
require_relative 'environment'
|
9
|
+
require_relative 'mysql_types'
|
8
10
|
require_relative 'sql_parser'
|
9
|
-
require_relative '
|
10
|
-
|
11
|
-
if ARGV.include?('-v') || ARGV.include?('--version')
|
12
|
-
puts File.read('.version')
|
13
|
-
exit
|
14
|
-
end
|
15
|
-
|
16
|
-
raise 'you must specify a configuration file' unless ARGV.size == 1
|
17
|
-
raise 'configuration file does not exist' unless File.exist?(ARGV[0])
|
11
|
+
require_relative 'sqlui'
|
18
12
|
|
19
13
|
# SQLUI Sinatra server.
|
20
14
|
class Server < Sinatra::Base
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
set :logging, true
|
32
|
-
set :bind, '0.0.0.0'
|
33
|
-
set :port, Environment.server_port
|
34
|
-
set :env, Environment.server_env
|
35
|
-
|
36
|
-
get '/-/health' do
|
37
|
-
status 200
|
38
|
-
body 'OK'
|
39
|
-
end
|
40
|
-
|
41
|
-
get "#{CONFIG.list_url_path}/?" do
|
42
|
-
erb :databases, locals: { config: @config }
|
43
|
-
end
|
44
|
-
|
45
|
-
CONFIG.database_configs.each do |database|
|
46
|
-
get database.url_path.to_s do
|
47
|
-
redirect "#{params[:database]}/app", 301
|
48
|
-
end
|
49
|
-
|
50
|
-
get "#{database.url_path}/app" do
|
51
|
-
@html ||= File.read(File.join(@resources_dir, 'sqlui.html'))
|
15
|
+
def self.init_and_run(config, resources_dir)
|
16
|
+
set :logging, true
|
17
|
+
set :bind, '0.0.0.0'
|
18
|
+
set :port, Environment.server_port
|
19
|
+
set :env, Environment.server_env
|
20
|
+
set :raise_errors, false
|
21
|
+
set :show_exceptions, false
|
22
|
+
|
23
|
+
get '/-/health' do
|
52
24
|
status 200
|
53
|
-
|
54
|
-
body @html
|
25
|
+
body 'OK'
|
55
26
|
end
|
56
27
|
|
57
|
-
get "#{
|
58
|
-
|
59
|
-
status 200
|
60
|
-
headers 'Content-Type': 'text/css'
|
61
|
-
body @css
|
28
|
+
get "#{config.list_url_path}/?" do
|
29
|
+
erb :databases, locals: { config: config }
|
62
30
|
end
|
63
31
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
body @js
|
69
|
-
end
|
32
|
+
config.database_configs.each do |database|
|
33
|
+
get database.url_path.to_s do
|
34
|
+
redirect "#{database.url_path}/app", 301
|
35
|
+
end
|
70
36
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
37
|
+
get "#{database.url_path}/app" do
|
38
|
+
@html ||= File.read(File.join(resources_dir, 'sqlui.html'))
|
39
|
+
status 200
|
40
|
+
headers 'Content-Type': 'text/html'
|
41
|
+
body @html
|
75
42
|
end
|
76
|
-
status 200
|
77
|
-
headers 'Content-Type': 'application/json'
|
78
|
-
body metadata.to_json
|
79
|
-
end
|
80
43
|
|
81
|
-
|
82
|
-
|
83
|
-
|
44
|
+
get "#{database.url_path}/sqlui.css" do
|
45
|
+
@css ||= File.read(File.join(resources_dir, 'sqlui.css'))
|
46
|
+
status 200
|
47
|
+
headers 'Content-Type': 'text/css'
|
48
|
+
body @css
|
49
|
+
end
|
84
50
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
51
|
+
get "#{database.url_path}/sqlui.js" do
|
52
|
+
@js ||= File.read(File.join(resources_dir, 'sqlui.js'))
|
53
|
+
status 200
|
54
|
+
headers 'Content-Type': 'text/javascript'
|
55
|
+
body @js
|
89
56
|
end
|
90
57
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
58
|
+
get "#{database.url_path}/metadata" do
|
59
|
+
database_config = config.database_config_for(url_path: database.url_path)
|
60
|
+
metadata = database_config.with_client do |client|
|
61
|
+
{
|
62
|
+
server: config.name,
|
63
|
+
schemas: DatabaseMetadata.lookup(client, database_config),
|
64
|
+
saved: Dir.glob("#{database_config.saved_path}/*.sql").map do |path|
|
65
|
+
comment_lines = File.readlines(path).take_while do |l|
|
66
|
+
l.start_with?('--')
|
67
|
+
end
|
68
|
+
description = comment_lines.map { |l| l.sub(/^-- */, '') }.join
|
69
|
+
{
|
70
|
+
filename: File.basename(path),
|
71
|
+
description: description
|
72
|
+
}
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
status 200
|
77
|
+
headers 'Content-Type': 'application/json'
|
78
|
+
body metadata.to_json
|
79
|
+
end
|
95
80
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
return client_error('missing cursor') unless params[:cursor]
|
81
|
+
get "#{database.url_path}/query_file" do
|
82
|
+
break client_error('missing file param') unless params[:file]
|
83
|
+
break client_error('no such file') unless File.exist?(params[:file])
|
100
84
|
|
101
|
-
|
102
|
-
|
85
|
+
database_config = config.database_config_for(url_path: database.url_path)
|
86
|
+
sql = File.read(File.join(database_config.saved_path, params[:file]))
|
87
|
+
result = database_config.with_client do |client|
|
88
|
+
execute_query(client, sql).tap { |r| r[:file] = params[:file] }
|
89
|
+
end
|
103
90
|
|
104
|
-
|
105
|
-
|
106
|
-
|
91
|
+
status 200
|
92
|
+
headers 'Content-Type': 'application/json'
|
93
|
+
body result.to_json
|
107
94
|
end
|
108
95
|
|
109
|
-
|
96
|
+
post "#{database.url_path}/query" do
|
97
|
+
params.merge!(JSON.parse(request.body.read, symbolize_names: true))
|
98
|
+
break client_error('missing sql') unless params[:sql]
|
99
|
+
|
100
|
+
sql = params[:sql]
|
101
|
+
selection = params[:selection]
|
102
|
+
if selection
|
103
|
+
selection = selection.split(':').map { |v| Integer(v) }
|
104
|
+
|
105
|
+
sql = if selection[0] == selection[1]
|
106
|
+
SqlParser.find_statement_at_cursor(params[:sql], selection[0])
|
107
|
+
else
|
108
|
+
params[:sql][selection[0], selection[1]]
|
109
|
+
end
|
110
|
+
break client_error("can't find query at selection") unless sql
|
111
|
+
end
|
112
|
+
|
113
|
+
database_config = config.database_config_for(url_path: database.url_path)
|
114
|
+
result = database_config.with_client do |client|
|
115
|
+
execute_query(client, sql)
|
116
|
+
end
|
117
|
+
|
118
|
+
result[:selection] = params[:selection]
|
119
|
+
|
120
|
+
status 200
|
121
|
+
headers 'Content-Type': 'application/json'
|
122
|
+
body result.to_json
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
error do |e|
|
127
|
+
status 500
|
110
128
|
headers 'Content-Type': 'application/json'
|
129
|
+
result = {
|
130
|
+
error: e.message,
|
131
|
+
stacktrace: e.backtrace.map { |b| b }.join("\n")
|
132
|
+
}
|
111
133
|
body result.to_json
|
112
134
|
end
|
135
|
+
|
136
|
+
run!
|
113
137
|
end
|
114
138
|
|
115
139
|
private
|
@@ -117,151 +141,33 @@ class Server < Sinatra::Base
|
|
117
141
|
def client_error(message, stacktrace: nil)
|
118
142
|
status(400)
|
119
143
|
headers('Content-Type': 'application/json')
|
120
|
-
body({
|
121
|
-
end
|
122
|
-
|
123
|
-
def load_metadata(client, database, saved_path)
|
124
|
-
result = {
|
125
|
-
server: @config.name,
|
126
|
-
schemas: {},
|
127
|
-
saved: Dir.glob("#{saved_path}/*.sql").map do |path|
|
128
|
-
{
|
129
|
-
filename: File.basename(path),
|
130
|
-
description: File.readlines(path).take_while { |l| l.start_with?('--') }.map { |l| l.sub(/^-- */, '') }.join
|
131
|
-
}
|
132
|
-
end
|
133
|
-
}
|
134
|
-
|
135
|
-
where_clause = if database
|
136
|
-
"where table_schema = '#{database}'"
|
137
|
-
else
|
138
|
-
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
139
|
-
end
|
140
|
-
column_result = client.query(
|
141
|
-
<<~SQL
|
142
|
-
select
|
143
|
-
table_schema,
|
144
|
-
table_name,
|
145
|
-
column_name,
|
146
|
-
data_type,
|
147
|
-
character_maximum_length,
|
148
|
-
is_nullable,
|
149
|
-
column_key,
|
150
|
-
column_default,
|
151
|
-
extra
|
152
|
-
from information_schema.columns
|
153
|
-
#{where_clause}
|
154
|
-
order by table_schema, table_name, column_name, ordinal_position;
|
155
|
-
SQL
|
156
|
-
)
|
157
|
-
column_result.each do |row|
|
158
|
-
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
159
|
-
table_schema = row[:table_schema]
|
160
|
-
unless result[:schemas][table_schema]
|
161
|
-
result[:schemas][table_schema] = {
|
162
|
-
tables: {}
|
163
|
-
}
|
164
|
-
end
|
165
|
-
table_name = row[:table_name]
|
166
|
-
tables = result[:schemas][table_schema][:tables]
|
167
|
-
unless tables[table_name]
|
168
|
-
tables[table_name] = {
|
169
|
-
indexes: {},
|
170
|
-
columns: {}
|
171
|
-
}
|
172
|
-
end
|
173
|
-
columns = result[:schemas][table_schema][:tables][table_name][:columns]
|
174
|
-
column_name = row[:column_name]
|
175
|
-
columns[column_name] = {} unless columns[column_name]
|
176
|
-
column = columns[column_name]
|
177
|
-
column[:name] = column_name
|
178
|
-
column[:data_type] = row[:data_type]
|
179
|
-
column[:length] = row[:character_maximum_length]
|
180
|
-
column[:allow_null] = row[:is_nullable]
|
181
|
-
column[:key] = row[:column_key]
|
182
|
-
column[:default] = row[:column_default]
|
183
|
-
column[:extra] = row[:extra]
|
184
|
-
end
|
185
|
-
|
186
|
-
where_clause = if database
|
187
|
-
"where table_schema = '#{database}'"
|
188
|
-
else
|
189
|
-
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
190
|
-
end
|
191
|
-
stats_result = client.query(
|
192
|
-
<<~SQL
|
193
|
-
select
|
194
|
-
table_schema,
|
195
|
-
table_name,
|
196
|
-
index_name,
|
197
|
-
seq_in_index,
|
198
|
-
non_unique,
|
199
|
-
column_name
|
200
|
-
from information_schema.statistics
|
201
|
-
#{where_clause}
|
202
|
-
order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
|
203
|
-
SQL
|
204
|
-
)
|
205
|
-
stats_result.each do |row|
|
206
|
-
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
207
|
-
table_schema = row[:table_schema]
|
208
|
-
tables = result[:schemas][table_schema][:tables]
|
209
|
-
table_name = row[:table_name]
|
210
|
-
indexes = tables[table_name][:indexes]
|
211
|
-
index_name = row[:index_name]
|
212
|
-
indexes[index_name] = {} unless indexes[index_name]
|
213
|
-
index = indexes[index_name]
|
214
|
-
column_name = row[:column_name]
|
215
|
-
index[column_name] = {}
|
216
|
-
column = index[column_name]
|
217
|
-
column[:name] = index_name
|
218
|
-
column[:seq_in_index] = row[:seq_in_index]
|
219
|
-
column[:non_unique] = row[:non_unique]
|
220
|
-
column[:column_name] = row[:column_name]
|
221
|
-
end
|
222
|
-
|
223
|
-
result
|
144
|
+
body({ error: message, stacktrace: stacktrace }.compact.to_json)
|
224
145
|
end
|
225
146
|
|
226
147
|
def execute_query(client, sql)
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
when Integer, Float
|
244
|
-
'number'
|
245
|
-
when Date
|
246
|
-
'date'
|
247
|
-
when Time
|
248
|
-
'datetime'
|
249
|
-
when TrueClass, FalseClass
|
250
|
-
'boolean'
|
251
|
-
else
|
252
|
-
# TODO: report an error
|
253
|
-
value.class.to_s
|
254
|
-
end
|
255
|
-
end
|
148
|
+
if sql.include?(';')
|
149
|
+
results = sql.split(/(?<=;)/).map { |current| client.query(current) }
|
150
|
+
result = results[-1]
|
151
|
+
else
|
152
|
+
result = client.query(sql)
|
153
|
+
end
|
154
|
+
# NOTE: the call to result.field_types must go before any other interaction with the result. Otherwise you will
|
155
|
+
# get a seg fault. Seems to be a bug in Mysql2.
|
156
|
+
if result
|
157
|
+
column_types = MysqlTypes.map_to_google_charts_types(result.field_types)
|
158
|
+
rows = result.map(&:values)
|
159
|
+
columns = result.first&.keys || []
|
160
|
+
else
|
161
|
+
column_types = []
|
162
|
+
rows = []
|
163
|
+
columns = []
|
256
164
|
end
|
257
165
|
{
|
258
166
|
query: sql,
|
259
167
|
columns: columns,
|
260
168
|
column_types: column_types,
|
261
169
|
total_rows: rows.size,
|
262
|
-
rows: rows.take(MAX_ROWS)
|
170
|
+
rows: rows.take(Sqlui::MAX_ROWS)
|
263
171
|
}
|
264
172
|
end
|
265
|
-
|
266
|
-
run!
|
267
173
|
end
|